diff --git a/.drone.yml b/.drone.yml index 105c3b958c695645890ff97ee38b08af0636e7ec..bc123a89bc7e27047df3a0db29f288cd24dfe97d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -29,6 +29,16 @@ services: environment: - SPARQL_UPDATE=true - DBA_PASSWORD=dba + selenium: + image: registry.fpfis.eu/fpfis/selenium:standalone-chrome-3.141.59-oxygen + environment: + - DISPLAY=:99 + - SE_OPTS=-debug + - DISPLAY=:99 + - SCREEN_WIDTH=1280 + - SCREEN_HEIGHT=800 + - NODE_MAX_INSTANCES=5 + - NODE_MAX_SESSION=5 pipeline: npm-build: diff --git a/composer.json b/composer.json index 8940139bca1506795a97c917603b84ab73daf69e..f429aed09a4ad13a1a5f957d69f15a4427453dde 100644 --- a/composer.json +++ b/composer.json @@ -16,18 +16,22 @@ "require-dev": { "composer/installers": "^1.11", "drupal/better_exposed_filters": "^5.0", + "drupal/composite_reference": "^2.1", "drupal/config_devel": "~1.2", "drupal/core-composer-scaffold": "^9.2", "drupal/core-dev": "^9.2", "drupal/ctools": "^3.7", "drupal/description_list_field": "^1.0@alpha", "drupal/drupal-extension": "~4.1", + "drupal/entity_reference_revisions": "^1.9", + "drupal/field_group": "^3.2", "drupal/file_link": "^2.0.6", "drupal/facets_form": "1.0.0-alpha2", "drupal/pathauto": "^1.8", "drupal/search_api": "^1.21", "drupal/search_api_autocomplete": "^1.5", "drupal/token": "^1.10", + "drupal/typed_link": "^2.0", "drush/drush": "^10.3", "easyrdf/easyrdf": "1.0.0 as 0.9.1", "egulias/email-validator": "^2.1.22 || ^3.0", @@ -37,6 +41,7 @@ "openeuropa/oe_authentication": "^1.4", "openeuropa/oe_contact_forms": "~1.1", "openeuropa/oe_content": "^2.8.0", + "openeuropa/oe_content_extra": "1.x-dev", "openeuropa/oe_corporate_blocks": "^4.4", "openeuropa/oe_media": "^1.14", "openeuropa/oe_multilingual": "^1.9", @@ -54,6 +59,10 @@ "drupal":{ "type": "composer", "url": "https://packages.drupal.org/8" + }, + "openeuropa/oe_content_extra": { + "type": "git", + "url": "https://github.com/openeuropa/oe_content_extra" } }, "autoload": { @@ -78,6 +87,11 @@ } } }, + "patches": { + "drupal/entity_reference_revisions": { + "https://www.drupal.org/project/entity_reference_revisions/issues/2937835": "https://www.drupal.org/files/issues/2021-03-26/entity_reference_revisions-field_formatter_label-2937835-36.patch" + } + }, "drupal-scaffold": { "locations": { "web-root": "./build" diff --git a/docker-compose.yml b/docker-compose.yml index 6eb4b18ebbe41fd004a78c696c1209515c778836..1b7cab0b7423b20bbfd58bc4387e2296661369f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,26 @@ services: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" # ports: # - 3306:3306 + + # If you would like to see what is going on you can run the following on your host: + # docker run --rm -p 4444:4444 -p 5900:5900 --network="host" selenium/standalone-chrome-debug:latest + # Newer version of this image might run into this issue: + # @link https://github.com/elgalu/docker-selenium/issues/20 + selenium: + image: selenium/standalone-chrome-debug:3.141.59-oxygen + environment: + - DISPLAY=:99 + - SE_OPTS=-debug + - SCREEN_WIDTH=1280 + - SCREEN_HEIGHT=800 + - VNC_NO_PASSWORD=1 + ports: + - '4444:4444' + - '5900:5900' + expose: + - '4444' + shm_size: 2g + node: image: node:14.17.3 user: "node" diff --git a/modules/oe_whitelabel_extra_project/README.md b/modules/oe_whitelabel_extra_project/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6e5341582be9d16d11938f3ce2db1b3f2c81aa17 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/README.md @@ -0,0 +1 @@ +# OpenEuropa Whitelabel Content Extra Project diff --git a/modules/oe_whitelabel_extra_project/config/install/core.date_format.oe_whitelabel_project_date.yml b/modules/oe_whitelabel_extra_project/config/install/core.date_format.oe_whitelabel_project_date.yml new file mode 100644 index 0000000000000000000000000000000000000000..42fc99eea373224fafd6f626f3ff5c4f216f58b0 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/install/core.date_format.oe_whitelabel_project_date.yml @@ -0,0 +1,7 @@ +langcode: en +status: true +dependencies: { } +id: oe_whitelabel_project_date +label: 'OE Whitelabel Project date' +locked: false +pattern: 'd F Y' diff --git a/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.full.yml b/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.full.yml new file mode 100644 index 0000000000000000000000000000000000000000..8088900d97dc568ca29f492036d81a5cd0431f46 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.full.yml @@ -0,0 +1,239 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.full + - field.field.node.oe_project.body + - field.field.node.oe_project.oe_cx_achievements_and_milestone + - field.field.node.oe_project.oe_cx_gallery + - field.field.node.oe_project.oe_cx_impacts + - field.field.node.oe_project.oe_cx_lead_contributors + - field.field.node.oe_project.oe_cx_objective + - field.field.node.oe_project.oe_departments + - field.field.node.oe_project.oe_documents + - field.field.node.oe_project.oe_featured_media + - field.field.node.oe_project.oe_project_budget + - field.field.node.oe_project.oe_project_budget_eu + - field.field.node.oe_project.oe_project_calls + - field.field.node.oe_project.oe_project_contact + - field.field.node.oe_project.oe_project_coordinators + - field.field.node.oe_project.oe_project_dates + - field.field.node.oe_project.oe_project_funding_programme + - field.field.node.oe_project.oe_project_locations + - field.field.node.oe_project.oe_project_participants + - field.field.node.oe_project.oe_project_result_files + - field.field.node.oe_project.oe_project_results + - field.field.node.oe_project.oe_project_website + - field.field.node.oe_project.oe_reference_code + - field.field.node.oe_project.oe_subject + - field.field.node.oe_project.oe_summary + - field.field.node.oe_project.oe_teaser + - node.type.oe_project + module: + - datetime_range + - entity_reference_revisions + - field_group + - link + - rdf_skos + - text + - user +third_party_settings: + field_group: + group_project_details: + children: + - group_website + - group_coordinators + label: 'Project details' + parent_name: '' + region: content + weight: 2 + format_type: html_element + format_settings: + classes: '' + show_empty_fields: false + id: '' + element: div + show_label: false + label_element: h3 + label_element_classes: 'fw-bold mb-4' + attributes: '' + effect: none + speed: fast + group_coordinators: + children: + - oe_project_coordinators + label: Coordinators + parent_name: group_project_details + region: content + weight: 3 + format_type: oe_whitelabel_helper_description_list_pattern + format_settings: { } + group_budget: + children: + - oe_project_budget + - oe_project_budget_eu + label: Budget + parent_name: '' + region: content + weight: 1 + format_type: html_element + format_settings: + classes: '' + show_empty_fields: false + id: '' + element: div + show_label: false + label_element: h3 + label_element_classes: '' + attributes: '' + effect: none + speed: fast + group_website: + children: + - oe_project_website + - oe_project_funding_programme + - oe_reference_code + label: Website + parent_name: group_project_details + region: content + weight: 2 + format_type: oe_whitelabel_helper_description_list_pattern + format_settings: { } +id: node.oe_project.full +targetEntityType: node +bundle: oe_project +mode: full +content: + oe_cx_achievements_and_milestone: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 8 + region: content + oe_cx_impacts: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 5 + region: content + oe_cx_lead_contributors: + type: entity_reference_revisions_entity_view + label: hidden + settings: + view_mode: default + link: '' + third_party_settings: { } + weight: 6 + region: content + oe_cx_objective: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 4 + region: content + oe_project_budget: + type: number_decimal + label: hidden + settings: + thousand_separator: . + decimal_separator: ',' + scale: 2 + prefix_suffix: true + third_party_settings: { } + weight: 2 + region: content + oe_project_budget_eu: + type: number_decimal + label: hidden + settings: + thousand_separator: . + decimal_separator: ',' + scale: 2 + prefix_suffix: true + third_party_settings: { } + weight: 3 + region: content + oe_project_coordinators: + type: entity_reference_revisions_label + label: hidden + settings: + link: false + third_party_settings: { } + weight: 4 + region: content + oe_project_dates: + type: daterange_default + label: hidden + settings: + timezone_override: '' + format_type: oe_whitelabel_project_date + separator: '-' + third_party_settings: { } + weight: 0 + region: content + oe_project_funding_programme: + type: skos_concept_entity_reference_label + label: hidden + settings: + link: false + third_party_settings: { } + weight: 4 + region: content + oe_project_participants: + type: entity_reference_revisions_entity_view + label: hidden + settings: + view_mode: default + link: '' + third_party_settings: { } + weight: 7 + region: content + oe_project_website: + type: link + label: hidden + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '' + target: '' + third_party_settings: { } + weight: 3 + region: content + oe_reference_code: + type: string + label: hidden + settings: + link_to_entity: false + third_party_settings: { } + weight: 5 + region: content + oe_summary: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 3 + region: content +hidden: + body: true + langcode: true + links: true + oe_content_content_owner: true + oe_content_legacy_link: true + oe_content_navigation_title: true + oe_content_short_title: true + oe_cx_gallery: true + oe_departments: true + oe_documents: true + oe_featured_media: true + oe_project_calls: true + oe_project_contact: true + oe_project_locations: true + oe_project_result_files: true + oe_project_results: true + oe_subject: true + oe_teaser: true diff --git a/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.oe_w_content_banner.yml b/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.oe_w_content_banner.yml new file mode 100644 index 0000000000000000000000000000000000000000..d553241623ac26a3c6e2ec7136062b1c251ffe4d --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.oe_w_content_banner.yml @@ -0,0 +1,85 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.oe_w_content_banner + - field.field.node.oe_project.body + - field.field.node.oe_project.oe_cx_achievements_and_milestone + - field.field.node.oe_project.oe_cx_gallery + - field.field.node.oe_project.oe_cx_impacts + - field.field.node.oe_project.oe_cx_lead_contributors + - field.field.node.oe_project.oe_cx_objective + - field.field.node.oe_project.oe_departments + - field.field.node.oe_project.oe_documents + - field.field.node.oe_project.oe_featured_media + - field.field.node.oe_project.oe_project_budget + - field.field.node.oe_project.oe_project_budget_eu + - field.field.node.oe_project.oe_project_calls + - field.field.node.oe_project.oe_project_contact + - field.field.node.oe_project.oe_project_coordinators + - field.field.node.oe_project.oe_project_dates + - field.field.node.oe_project.oe_project_funding_programme + - field.field.node.oe_project.oe_project_locations + - field.field.node.oe_project.oe_project_participants + - field.field.node.oe_project.oe_project_result_files + - field.field.node.oe_project.oe_project_results + - field.field.node.oe_project.oe_project_website + - field.field.node.oe_project.oe_reference_code + - field.field.node.oe_project.oe_subject + - field.field.node.oe_project.oe_summary + - field.field.node.oe_project.oe_teaser + - node.type.oe_project + module: + - rdf_skos + - text + - user +id: node.oe_project.oe_w_content_banner +targetEntityType: node +bundle: oe_project +mode: oe_w_content_banner +content: + oe_subject: + type: skos_concept_entity_reference_label + label: hidden + settings: + link: false + third_party_settings: { } + weight: 1 + region: content + oe_teaser: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 0 + region: content +hidden: + body: true + langcode: true + links: true + oe_content_content_owner: true + oe_content_legacy_link: true + oe_content_navigation_title: true + oe_content_short_title: true + oe_cx_achievements_and_milestone: true + oe_cx_gallery: true + oe_cx_impacts: true + oe_cx_lead_contributors: true + oe_cx_objective: true + oe_departments: true + oe_documents: true + oe_featured_media: true + oe_project_budget: true + oe_project_budget_eu: true + oe_project_calls: true + oe_project_contact: true + oe_project_coordinators: true + oe_project_dates: true + oe_project_funding_programme: true + oe_project_locations: true + oe_project_participants: true + oe_project_result_files: true + oe_project_results: true + oe_project_website: true + oe_reference_code: true + oe_summary: true diff --git a/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.teaser.yml b/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.teaser.yml new file mode 100644 index 0000000000000000000000000000000000000000..acf603362a50b8e93542177dc9aebf588f60330a --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/install/core.entity_view_display.node.oe_project.teaser.yml @@ -0,0 +1,124 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.teaser + - field.field.node.oe_project.body + - field.field.node.oe_project.oe_cx_achievements_and_milestone + - field.field.node.oe_project.oe_cx_gallery + - field.field.node.oe_project.oe_cx_impacts + - field.field.node.oe_project.oe_cx_lead_contributors + - field.field.node.oe_project.oe_cx_objective + - field.field.node.oe_project.oe_departments + - field.field.node.oe_project.oe_documents + - field.field.node.oe_project.oe_featured_media + - field.field.node.oe_project.oe_project_budget + - field.field.node.oe_project.oe_project_budget_eu + - field.field.node.oe_project.oe_project_calls + - field.field.node.oe_project.oe_project_contact + - field.field.node.oe_project.oe_project_coordinators + - field.field.node.oe_project.oe_project_dates + - field.field.node.oe_project.oe_project_funding_programme + - field.field.node.oe_project.oe_project_locations + - field.field.node.oe_project.oe_project_participants + - field.field.node.oe_project.oe_project_result_files + - field.field.node.oe_project.oe_project_results + - field.field.node.oe_project.oe_project_website + - field.field.node.oe_project.oe_reference_code + - field.field.node.oe_project.oe_subject + - field.field.node.oe_project.oe_summary + - field.field.node.oe_project.oe_teaser + - node.type.oe_project + module: + - datetime_range + - field_group + - oe_content_featured_media_field + - rdf_skos + - text + - user +third_party_settings: + field_group: + group_stakeholders: + children: + - oe_project_participants + label: Stakeholders + parent_name: '' + region: hidden + weight: 30 + format_type: html_element + format_settings: + classes: '' + id: '' + element: div + show_label: false + label_element: h3 + label_element_classes: '' + attributes: '' + effect: none + speed: fast +id: node.oe_project.teaser +targetEntityType: node +bundle: oe_project +mode: teaser +content: + oe_featured_media: + type: oe_featured_media_label + label: hidden + settings: + link: true + third_party_settings: { } + weight: 0 + region: content + oe_project_dates: + type: daterange_default + label: hidden + settings: + timezone_override: '' + format_type: oe_whitelabel_project_date + separator: '-' + third_party_settings: { } + weight: 3 + region: content + oe_subject: + type: skos_concept_entity_reference_label + label: hidden + settings: + link: false + third_party_settings: { } + weight: 2 + region: content + oe_teaser: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 1 + region: content +hidden: + body: true + langcode: true + links: true + oe_content_content_owner: true + oe_content_legacy_link: true + oe_content_navigation_title: true + oe_content_short_title: true + oe_cx_achievements_and_milestone: true + oe_cx_gallery: true + oe_cx_impacts: true + oe_cx_lead_contributors: true + oe_cx_objective: true + oe_departments: true + oe_documents: true + oe_project_budget: true + oe_project_budget_eu: true + oe_project_calls: true + oe_project_contact: true + oe_project_coordinators: true + oe_project_funding_programme: true + oe_project_locations: true + oe_project_participants: true + oe_project_result_files: true + oe_project_results: true + oe_project_website: true + oe_reference_code: true + oe_summary: true diff --git a/modules/oe_whitelabel_extra_project/config/overrides/core.entity_form_display.node.oe_project.default.yml b/modules/oe_whitelabel_extra_project/config/overrides/core.entity_form_display.node.oe_project.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..cf518277a60ebcf6288ea70723e165a31b5ed7d6 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/overrides/core.entity_form_display.node.oe_project.default.yml @@ -0,0 +1,337 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.oe_project.body + - field.field.node.oe_project.oe_cx_achievements_and_milestone + - field.field.node.oe_project.oe_cx_gallery + - field.field.node.oe_project.oe_cx_impacts + - field.field.node.oe_project.oe_cx_lead_contributors + - field.field.node.oe_project.oe_cx_objective + - field.field.node.oe_project.oe_departments + - field.field.node.oe_project.oe_documents + - field.field.node.oe_project.oe_featured_media + - field.field.node.oe_project.oe_project_budget + - field.field.node.oe_project.oe_project_budget_eu + - field.field.node.oe_project.oe_project_calls + - field.field.node.oe_project.oe_project_contact + - field.field.node.oe_project.oe_project_coordinators + - field.field.node.oe_project.oe_project_dates + - field.field.node.oe_project.oe_project_funding_programme + - field.field.node.oe_project.oe_project_locations + - field.field.node.oe_project.oe_project_participants + - field.field.node.oe_project.oe_project_result_files + - field.field.node.oe_project.oe_project_results + - field.field.node.oe_project.oe_project_website + - field.field.node.oe_project.oe_reference_code + - field.field.node.oe_project.oe_subject + - field.field.node.oe_project.oe_summary + - field.field.node.oe_project.oe_teaser + - node.type.oe_project + module: + - datetime_range + - field_group + - inline_entity_form + - link + - oe_content_featured_media_field + - rdf_skos + - text +third_party_settings: + field_group: + group_result: + children: + - oe_project_results + - oe_project_result_files + label: Result + region: content + parent_name: '' + weight: 13 + format_type: details + format_settings: + classes: '' + id: '' + open: true + description: '' + required_fields: true + group_budget: + children: + - oe_project_budget + - oe_project_budget_eu + label: Budget + region: content + parent_name: group_details + weight: 6 + format_type: tab + format_settings: + classes: '' + id: '' + formatter: open + description: '' + required_fields: true + group_details: + children: + - oe_project_dates + - group_budget + - oe_project_website + - oe_project_funding_programme + - oe_reference_code + - oe_project_coordinators + label: 'Project details' + region: content + parent_name: '' + weight: 4 + format_type: html_element + format_settings: + classes: '' + show_empty_fields: false + id: '' + element: div + show_label: true + label_element: h3 + label_element_classes: '' + attributes: '' + effect: none + speed: fast + required_fields: true +id: node.oe_project.default +targetEntityType: node +bundle: oe_project +mode: default +content: + oe_cx_achievements_and_milestone: + type: text_textarea + weight: 10 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + oe_cx_gallery: + type: entity_reference_autocomplete + weight: 11 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + oe_cx_impacts: + type: text_textarea + weight: 7 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + oe_cx_lead_contributors: + type: inline_entity_form_complex + weight: 9 + region: content + settings: + form_mode: default + override_labels: false + label_singular: '' + label_plural: '' + allow_new: true + allow_existing: false + match_operator: CONTAINS + allow_duplicate: false + collapsible: false + collapsed: false + revision: false + removed_reference: optional + third_party_settings: { } + oe_cx_objective: + type: text_textarea + weight: 6 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + oe_documents: + type: entity_reference_autocomplete + weight: 12 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + oe_featured_media: + type: oe_featured_media_autocomplete + weight: 1 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + oe_project_budget: + type: number + weight: 9 + region: content + settings: + placeholder: '' + third_party_settings: { } + oe_project_budget_eu: + type: number + weight: 10 + region: content + settings: + placeholder: '' + third_party_settings: { } + oe_project_coordinators: + type: inline_entity_form_complex + weight: 10 + region: content + settings: + form_mode: default + override_labels: false + label_singular: '' + label_plural: '' + allow_new: true + allow_existing: false + match_operator: CONTAINS + allow_duplicate: false + collapsible: false + collapsed: false + revision: false + removed_reference: optional + third_party_settings: { } + oe_project_dates: + type: daterange_datelist + weight: 5 + region: content + settings: + increment: 15 + date_order: DMY + time_type: none + third_party_settings: { } + oe_project_funding_programme: + type: skos_concept_entity_reference_autocomplete + weight: 8 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + oe_project_participants: + type: inline_entity_form_complex + weight: 8 + region: content + settings: + form_mode: default + override_labels: true + label_singular: participant + label_plural: participants + allow_new: true + allow_existing: false + match_operator: CONTAINS + allow_duplicate: false + collapsible: true + collapsed: false + revision: true + removed_reference: keep + third_party_settings: { } + oe_project_result_files: + type: entity_reference_autocomplete + weight: 0 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + oe_project_results: + type: text_textarea + weight: 0 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + oe_project_website: + type: link_default + weight: 7 + region: content + settings: + placeholder_url: '' + placeholder_title: '' + third_party_settings: { } + oe_reference_code: + type: string_textfield + weight: 9 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + oe_subject: + type: skos_concept_entity_reference_autocomplete + weight: 3 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + oe_summary: + type: text_textarea + weight: 5 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + oe_teaser: + type: text_textarea + weight: 2 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + status: + type: boolean_checkbox + weight: 14 + region: content + settings: + display_label: true + third_party_settings: { } + title: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + translation: + weight: 10 + region: content + settings: { } + third_party_settings: { } +hidden: + body: true + created: true + langcode: true + oe_content_content_owner: true + oe_content_legacy_link: true + oe_content_navigation_title: true + oe_content_short_title: true + oe_departments: true + oe_project_calls: true + oe_project_contact: true + oe_project_locations: true + path: true + promote: true + sticky: true + uid: true diff --git a/modules/oe_whitelabel_extra_project/config/overrides/core.entity_form_display.oe_organisation.oe_cx_project_stakeholder.default.yml b/modules/oe_whitelabel_extra_project/config/overrides/core.entity_form_display.oe_organisation.oe_cx_project_stakeholder.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..db59f56bf43ac5cfcbd01d5a32a184b7b40733aa --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/overrides/core.entity_form_display.oe_organisation.oe_cx_project_stakeholder.default.yml @@ -0,0 +1,54 @@ +langcode: en +status: true +dependencies: + config: + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_acronym + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_address + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_contact_url + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_cx_contribution_budget + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_logo + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_website + - oe_content_entity_organisation.oe_organisation_type.oe_cx_project_stakeholder + module: + - address +id: oe_organisation.oe_cx_project_stakeholder.default +targetEntityType: oe_organisation +bundle: oe_cx_project_stakeholder +mode: default +content: + name: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + oe_acronym: + type: string_textfield + weight: 1 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + oe_address: + type: address_default + weight: 2 + region: content + settings: { } + third_party_settings: { } + oe_cx_contribution_budget: + type: number + weight: 3 + region: content + settings: + placeholder: '' + third_party_settings: { } +hidden: + created: true + langcode: true + oe_contact_url: true + oe_logo: true + oe_website: true + status: true diff --git a/modules/oe_whitelabel_extra_project/config/overrides/core.entity_view_display.oe_organisation.oe_cx_project_stakeholder.default.yml b/modules/oe_whitelabel_extra_project/config/overrides/core.entity_view_display.oe_organisation.oe_cx_project_stakeholder.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..f1faedb5a95ea9d2c224118877ba2d9ab3ed7d8d --- /dev/null +++ b/modules/oe_whitelabel_extra_project/config/overrides/core.entity_view_display.oe_organisation.oe_cx_project_stakeholder.default.yml @@ -0,0 +1,74 @@ +langcode: en +status: true +dependencies: + config: + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_acronym + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_address + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_contact_url + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_cx_contribution_budget + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_logo + - field.field.oe_organisation.oe_cx_project_stakeholder.oe_website + - oe_content_entity_organisation.oe_organisation_type.oe_cx_project_stakeholder + module: + - field_group + - oe_whitelabel_helper +third_party_settings: + field_group: + group_info: + children: + - name + - oe_address + - oe_cx_contribution_budget + label: Info + parent_name: '' + region: content + weight: 1 + format_type: oe_whitelabel_helper_description_list_pattern + format_settings: { } +id: oe_organisation.oe_cx_project_stakeholder.default +targetEntityType: oe_organisation +bundle: oe_cx_project_stakeholder +mode: default +content: + name: + type: string + label: hidden + settings: + link_to_entity: false + third_party_settings: { } + weight: 1 + region: content + oe_acronym: + type: string + label: hidden + settings: + link_to_entity: false + third_party_settings: { } + weight: 0 + region: content + oe_address: + type: oe_whitelabel_helper_address_inline + label: hidden + settings: + delimiter: ', ' + third_party_settings: { } + weight: 2 + region: content + oe_cx_contribution_budget: + type: number_decimal + label: hidden + settings: + thousand_separator: . + decimal_separator: ',' + scale: 2 + prefix_suffix: true + third_party_settings: { } + weight: 4 + region: content +hidden: + created: true + langcode: true + oe_contact_url: true + oe_logo: true + oe_website: true + status: true diff --git a/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.info.yml b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..0ec08ee9175241b1bd4026ca9c5c04b13e4ab8a9 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.info.yml @@ -0,0 +1,15 @@ +name: OpenEuropa Whitelabel Content Extra Project +type: module +description: Module to override content extra project. +package: OpenEuropa Whitelabel Theme +core_version_requirement: ^9.2 +dependencies: + - oe_content_extra:oe_content_extra_project + - oe_whitelabel_helper:oe_whitelabel_helper + +config_devel: + install: + - core.date_format.oe_whitelabel_project_date + - core.entity_view_display.node.oe_project.full + - core.entity_view_display.node.oe_project.oe_w_content_banner + - core.entity_view_display.node.oe_project.teaser diff --git a/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.install b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.install new file mode 100644 index 0000000000000000000000000000000000000000..782c6ebd1c84569d254b203811d49468767210b0 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.install @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Install and update functions for the whitelabel Extra Project module. + */ + +declare(strict_types = 1); + +use Drupal\oe_bootstrap_theme\ConfigImporter; + +/** + * Implements hook_install(). + * + * Customise fields for content project. + */ +function oe_whitelabel_extra_project_install($is_syncing): void { + // If we are installing from config, we bail out. + if ($is_syncing) { + return; + } + + $configs = [ + 'core.entity_form_display.node.oe_project.default', + 'core.entity_form_display.oe_organisation.oe_cx_project_stakeholder.default', + 'core.entity_view_display.oe_organisation.oe_cx_project_stakeholder.default', + ]; + + ConfigImporter::importMultiple('module', 'oe_whitelabel_extra_project', '/config/overrides/', $configs); +} diff --git a/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module new file mode 100644 index 0000000000000000000000000000000000000000..2300a0dcd13f54458d92c55f718e023ed04082a0 --- /dev/null +++ b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module @@ -0,0 +1,319 @@ +<?php + +/** + * @file + * OE Whitelabel theme extra project. + */ + +declare(strict_types = 1); + +use Drupal\Component\Utility\Html; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Template\Attribute; +use Drupal\media\MediaInterface; +use Drupal\media\Plugin\media\Source\Image; +use Drupal\media\Plugin\media\Source\OEmbed; +use Drupal\oe_bootstrap_theme\ValueObject\ImageValueObject; + +/** + * Implements hook_preprocess_HOOK() for "pattern_description_list". + * + * Adds a bottom border for some instances of this pattern that are used in + * field groups on the project detail page. + */ +function oe_whitelabel_extra_project_preprocess_pattern_description_list(array &$variables): void { + /** @var \Drupal\ui_patterns\Element\PatternContext $context */ + $context = $variables['context']; + if ($context->getType() === 'field_group') { + $id = $context->getProperty('entity_type') + . '.' . $context->getProperty('bundle') + . '.' . $context->getProperty('view_mode') + . '.' . $context->getProperty('group_name'); + switch ($id) { + case 'oe_organisation.oe_cx_project_stakeholder.default.group_info': + case 'node.oe_project.full.group_project_details': + case 'node.oe_project.full.group_coordinators': + case 'node.oe_project.full.group_period': + case 'node.oe_project.full.group_budget': + case 'node.oe_project.full.group_website': + /** @var \Drupal\Core\Template\Attribute $attributes */ + $attributes = $variables['attributes']; + $attributes->addClass('border-bottom'); + $attributes->addClass('pb-3'); + break; + } + } +} + +/** + * Implements hook_preprocess_node() for the project full view mode. + */ +function oe_whitelabel_extra_project_preprocess_node__oe_project__full(array &$variables): void { + _oe_whitelabel_extra_project_preprocess_inpage_nav($variables); + _oe_whitelabel_extra_project_preprocess_status_and_progress($variables); + _oe_whitelabel_extra_project_preprocess_contributions($variables); +} + +/** + * Implements hook_preprocess_node() for the project content banner view mode. + */ +function oe_whitelabel_extra_project_preprocess_node__oe_project__oe_w_content_banner(array &$variables): void { + _oe_whitelabel_extra_project_preprocess_featured_media($variables); +} + +/** + * Implements hook_preprocess_node() for the project teaser. + */ +function oe_whitelabel_extra_project_preprocess_node__oe_project__teaser(array &$variables): void { + _oe_whitelabel_extra_project_preprocess_featured_media($variables); +} + +/** + * Creates an image value object in $variables['image']. + * + * @param array $variables + * Variables from hook_preprocess_node(). + */ +function _oe_whitelabel_extra_project_preprocess_featured_media(array &$variables): void { + /** @var \Drupal\node\NodeInterface $node */ + $node = $variables['node']; + + // Bail out if there is no media present. + if ($node->get('oe_featured_media')->isEmpty()) { + return; + } + + /** @var \Drupal\media\Entity\Media $media */ + $media = $node->get('oe_featured_media')->entity; + if (!$media instanceof MediaInterface) { + // The media entity is not available anymore, bail out. + return; + } + + // Retrieve the correct media translation. + /** @var \Drupal\media\Entity\Media $media */ + $media = \Drupal::service('entity.repository')->getTranslationFromContext($media, $node->language()->getId()); + + // Caches are handled by the formatter usually. Since we are not rendering + // the original render arrays, we need to propagate our caches to the + // paragraph template. + $cacheability = CacheableMetadata::createFromRenderArray($variables); + $cacheability->addCacheableDependency($media); + + // Run access checks on the media entity. + $access = $media->access('view', $variables['user'], TRUE); + $cacheability->addCacheableDependency($access); + if (!$access->isAllowed()) { + $cacheability->applyTo($variables); + return; + } + + // Get the media source. + $source = $media->getSource(); + + if (!$source instanceof OEmbed && !$source instanceof Image) { + // Media is not a video or image, no thumbnail will be shown. + $cacheability->applyTo($variables); + return; + } + + /** @var \Drupal\image\Plugin\Field\FieldType\ImageItem $thumbnail */ + $thumbnail = $media->get('thumbnail')->first(); + $variables['image'] = ImageValueObject::fromImageItem($thumbnail); + + $cacheability->applyTo($variables); +} + +/** + * Helper function to preprocess the inpage navigation pattern fields. + * + * @param array $variables + * Variables from hook_preprocess_node(). + */ +function _oe_whitelabel_extra_project_preprocess_inpage_nav(array &$variables): void { + /** @var \Drupal\node\NodeInterface $node */ + $node = $variables['node']; + $variables['inpage_navigation_links'] = []; + + $fields = [ + 'oe_summary', + 'oe_cx_objective', + 'oe_cx_impacts', + 'oe_cx_lead_contributors', + 'oe_project_participants', + 'oe_cx_achievements_and_milestone', + ]; + foreach ($variables['content'] as &$item) { + if (!array_key_exists('#field_name', $item)) { + continue; + } + + if (!in_array($item['#field_name'], $fields)) { + continue; + } + + $unique_id = Html::getUniqueId('oe-project--' . $item['#field_name']); + $item['#inpage_nav_id'] = $unique_id; + $variables['inpage_navigation_links'][] = [ + 'path' => '#' . $unique_id, + 'label' => $node->{$item['#field_name']}->getFieldDefinition()->getLabel(), + ]; + } +} + +/** + * Adds variables for the project status. + * + * @param array $variables + * Variables from hook_preprocess_node(). + */ +function _oe_whitelabel_extra_project_preprocess_status_and_progress(array &$variables): void { + /** @var \Drupal\node\NodeInterface $node */ + $node = $variables['node']; + /** @var \Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem|null $date_range_item */ + $date_range_item = $node->get('oe_project_dates')->first(); + if ($date_range_item === NULL || !$date_range_item->value || !$date_range_item->end_value) { + // One of the fields is empty. + return; + } + + // Dates only store the date, not the time. + // Use the site-wide configured timezone, not a user-specific timezone. + /* @see \Drupal\system\TimeZoneResolver::getTimeZone() */ + $system_date_config = \Drupal::config('system.date'); + /** @var string $timezone */ + $timezone = $system_date_config->get('timezone.default') ?? 'UTC'; + + // Invalidate cache when site timezone is changed. + CacheableMetadata::createFromRenderArray($variables) + ->addCacheableDependency($system_date_config) + ->applyTo($variables); + + $get_timestamp = static function (string $date_string) use ($timezone): int { + return (new DrupalDateTime($date_string, $timezone))->getTimestamp(); + }; + // Project starts at the beginning of the first day at 00:00. + $t_start = $get_timestamp($date_range_item->value); + // Project ends at the end of the last day at 24:00. + $t_end = $get_timestamp($date_range_item->end_value . ' +1 day'); + + if ($t_start >= $t_end) { + // Invalid date range. No progress or status can be shown. + return; + } + + // Use the formatted field values for start / end date. + $element = $variables['elements']['oe_project_dates'][0] ?? []; + if ($element['#theme'] ?? NULL === 'time') { + // Project lasts a single day. + $start_date_element = $element; + $end_date_element = $element; + } + elseif (isset($element['start_date']['#theme'], $element['end_date']['#theme'])) { + // Project lasts multiple days. + $start_date_element = $element['start_date']; + $end_date_element = $element['end_date']; + } + else { + // Empty or incomplete date range. No progress or status can be shown. + return; + } + + $status_labels = [t('Planned'), t('Ongoing'), t('Closed')]; + + // Values for the 'bcl-project-status' component. + // Some values contain placeholders that will be updated with javascript. + // This makes sure that tests will fail if js does not run. + $variables['project_status_args'] = [ + // Placeholder value. + 'status' => 'planned', + 'start_date' => $start_date_element, + 'start_label' => t('Start'), + 'end_date' => $end_date_element, + 'end_label' => t('End'), + 'label' => t('Status'), + // Placeholder value. + 'badge' => '&ellipsis;', + // Placeholder value, identical to 'planned'. + 'progress' => 0, + 'attributes' => new Attribute([ + 'data-start-timestamp' => $t_start, + 'data-end-timestamp' => $t_end, + 'data-status-labels' => implode('|', $status_labels), + // Hide for non-js users, to avoid showing wrong/outdated information. + 'class' => ['d-none'], + ]), + ]; +} + +/** + * Adds variables for the project contributions chart. + * + * @param array $variables + * Variables from hook_preprocess_node(). + */ +function _oe_whitelabel_extra_project_preprocess_contributions(array &$variables): void { + $field_bg_classes = [ + 'oe_project_budget' => 'bg-gray-400', + 'oe_project_budget_eu' => 'bg-primary', + ]; + $legend_items = []; + foreach ($field_bg_classes as $field_name => $bg_class) { + $field_element = $variables['elements'][$field_name] ?? NULL; + if (!isset($field_element[0])) { + continue; + } + $legend_items[] = [ + 'term' => [ + [ + 'label' => $field_element['#title'], + 'color' => $bg_class, + ], + ], + 'definition' => [ + [ + // Render only the field value, without field wrappers. + 'label' => $field_element[0], + ], + ], + ]; + } + + if (!$legend_items) { + return; + } + + $variables['contributions_args'] = [ + 'corporate_contributions' => NULL, + 'chart' => FALSE, + 'legend' => [ + 'variant' => 'horizontal', + 'items' => $legend_items, + ], + ]; + + /** @var \Drupal\node\NodeInterface $node */ + $node = $variables['node']; + $overall_budget = $node->get('oe_project_budget')->value; + $eu_budget = $node->get('oe_project_budget_eu')->value; + + if ($overall_budget === NULL + || $eu_budget === NULL + || $overall_budget <= 0 + || $eu_budget < 0 + || $overall_budget < $eu_budget + ) { + // No pie chart can be drawn with these values. + return; + } + + // The ratio will be in the range of 0..1, thanks to the checks above. + $ratio_01 = $eu_budget / $overall_budget; + // Convert to percent. + // The pie chart only supports multiples of 10%. + $percent = (int) round($ratio_01 * 10) * 10; + + $variables['contributions_args']['corporate_contributions'] = $percent; + $variables['contributions_args']['chart'] = TRUE; +} diff --git a/modules/oe_whitelabel_helper/config/schema/oe_whitelabel_helper.schema.yml b/modules/oe_whitelabel_helper/config/schema/oe_whitelabel_helper.schema.yml index df4af465acd511f0073361be579047d19cb767d9..2f171640dc7aa4fc5a609b8ef57abdcf1b7fd092 100644 --- a/modules/oe_whitelabel_helper/config/schema/oe_whitelabel_helper.schema.yml +++ b/modules/oe_whitelabel_helper/config/schema/oe_whitelabel_helper.schema.yml @@ -10,3 +10,16 @@ condition.plugin.oe_whitelabel_helper_current_component_library: mapping: component_library: type: string +field_group.field_group_formatter_plugin.oe_whitelabel_helper_pattern_base: + type: field_group.field_group_formatter_plugin.base + label: 'Mapping for the base pattern formatter settings' + mapping: + label: + type: label + label: 'Field group label' + variant: + type: string + label: 'Pattern variant' +field_group.field_group_formatter_plugin.oe_whitelabel_helper_description_list_pattern: + type: field_group.field_group_formatter_plugin.oe_whitelabel_helper_pattern_base + label: 'Mapping for the description list pattern formatter settings' diff --git a/modules/oe_whitelabel_helper/src/Plugin/field_group/FieldGroupFormatter/DescriptionListPattern.php b/modules/oe_whitelabel_helper/src/Plugin/field_group/FieldGroupFormatter/DescriptionListPattern.php new file mode 100644 index 0000000000000000000000000000000000000000..85c9194676624f1d229411a588889ab21250d7f1 --- /dev/null +++ b/modules/oe_whitelabel_helper/src/Plugin/field_group/FieldGroupFormatter/DescriptionListPattern.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\oe_whitelabel_helper\Plugin\field_group\FieldGroupFormatter; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Render\Element; +use Drupal\Core\Render\RendererInterface; +use Drupal\ui_patterns\UiPatternsManager; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Format a field group using the description list pattern. + * + * @FieldGroupFormatter( + * id = "oe_whitelabel_helper_description_list_pattern", + * label = @Translation("Description list pattern"), + * description = @Translation("Format a field group using the description list pattern."), + * supported_contexts = { + * "view" + * } + * ) + */ +class DescriptionListPattern extends PatternFormatterBase { + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs a DescriptionList object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + * @param \Drupal\ui_patterns\UiPatternsManager $patterns_manager + * The pattern manager. + */ + public function __construct(array $configuration, string $plugin_id, array $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, UiPatternsManager $patterns_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $patterns_manager); + $this->entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('renderer'), + $container->get('plugin.manager.ui_patterns') + ); + } + + /** + * {@inheritdoc} + */ + protected function getPatternId(): string { + return 'description_list'; + } + + /** + * {@inheritdoc} + */ + public function preRender(&$element, $rendering_object) { + parent::preRender($element, $rendering_object); + // Only support horizontal mode in this field group formatter. + $element['pattern']['#settings']['orientation'] = 'horizontal'; + } + + /** + * {@inheritdoc} + */ + protected function getFields(array &$element, $rendering_object): ?array { + $fields = []; + + foreach (Element::children($element) as $field_name) { + $field_element = $element[$field_name]; + $field_element['#label_display'] = 'hidden'; + $field_markup = $this->renderer->render($field_element); + if (trim((string) $field_markup) === '') { + continue; + } + // Assign field label and content to the pattern's fields. + $fields['items'][] = [ + 'term' => $field_element['#title'] ?? '', + 'definition' => $field_markup, + ]; + } + + if (empty($fields['items'])) { + return NULL; + } + + return $fields; + } + +} diff --git a/modules/oe_whitelabel_helper/src/Plugin/field_group/FieldGroupFormatter/PatternFormatterBase.php b/modules/oe_whitelabel_helper/src/Plugin/field_group/FieldGroupFormatter/PatternFormatterBase.php new file mode 100644 index 0000000000000000000000000000000000000000..da15223c754f0c61a8818ddc940f29952c3b17a0 --- /dev/null +++ b/modules/oe_whitelabel_helper/src/Plugin/field_group/FieldGroupFormatter/PatternFormatterBase.php @@ -0,0 +1,172 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\oe_whitelabel_helper\Plugin\field_group\FieldGroupFormatter; + +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Render\Element; +use Drupal\field_group\FieldGroupFormatterBase; +use Drupal\ui_patterns\UiPatternsManager; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Base class for field group formatters that use a pattern for rendering. + * + * Field group formatters extending this class generate a pattern context that + * is compatible with the one generated by the ui_patterns_field_group module. + * + * If you need to override pattern templates based on node, bundle or view mode + * just enable the ui_patterns_field_group module. + * + * @see https://ui-patterns.readthedocs.io/en/8.x-1.x/content/developer-documentation.html#working-with-pattern-suggestions + */ +abstract class PatternFormatterBase extends FieldGroupFormatterBase implements ContainerFactoryPluginInterface { + + /** + * UI Patterns manager. + * + * @var \Drupal\ui_patterns\UiPatternsManager + */ + protected $patternsManager; + + /** + * PatternFormatterBase constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\ui_patterns\UiPatternsManager $patterns_manager + * UI Patterns manager. + */ + public function __construct(array $configuration, string $plugin_id, array $plugin_definition, UiPatternsManager $patterns_manager) { + parent::__construct($plugin_id, $plugin_definition, $configuration['group'], $configuration['settings'], $configuration['label']); + $this->configuration = $configuration; + $this->patternsManager = $patterns_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.ui_patterns') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'label' => '', + 'variant' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function settingsForm() { + $pattern = $this->patternsManager->getDefinition($this->getPatternId()); + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Field group label'), + '#default_value' => $this->label, + ]; + + if ($pattern->hasVariants()) { + $form['variant'] = [ + '#title' => $this->t('Variant'), + '#type' => 'select', + '#options' => $pattern->getVariantsAsOptions(), + '#default_value' => $this->getSetting('variant'), + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + + if ($this->getSetting('label')) { + $summary[] = $this->t('Label: @label', ['@label' => $this->getSetting('label')]); + } + + if ($this->getSetting('variant')) { + $summary[] = $this->t('Variant: @variant', ['@variant' => $this->getSetting('variant')]); + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function preRender(&$element, $rendering_object) { + parent::preRender($element, $rendering_object); + + $fields = $this->getFields($element, $rendering_object); + if ($fields === NULL) { + // Don't render the pattern. + return; + } + // Instantiate the pattern render array. + $pattern = [ + '#type' => 'pattern', + '#id' => $this->getPatternId(), + '#variant' => $this->getSetting('variant'), + '#fields' => $fields, + '#context' => [ + 'type' => 'field_group', + 'group_name' => $element['#group_name'], + 'entity_type' => $element['#entity_type'], + 'bundle' => $element['#bundle'], + 'view_mode' => $this->group->mode, + ], + ]; + + // Remove all renderable elements, while keeping render metadata as that can + // be used to further manipulate the render array. + foreach (Element::children($element) as $key) { + unset($element[$key]); + } + $element += [ + 'pattern' => $pattern, + ]; + } + + /** + * Return pattern ID for the current formatter plugin. + * + * @return string + * Pattern ID. + */ + abstract protected function getPatternId(): string; + + /** + * Return list of fields for the current pattern. + * + * @param array $element + * Field group render element. + * @param object $rendering_object + * Field group rendering object. + * + * @return array|null + * Pattern fields to be rendered, or NULL if the field group + * should not be displayed at all. + */ + abstract protected function getFields(array &$element, $rendering_object): ?array; + +} diff --git a/modules/oe_whitelabel_helper/tests/src/Functional/Plugin/field_group/PatternFormatterTest.php b/modules/oe_whitelabel_helper/tests/src/Functional/Plugin/field_group/PatternFormatterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d00675ce3230351cdf58906cba8df38fc03b38ba --- /dev/null +++ b/modules/oe_whitelabel_helper/tests/src/Functional/Plugin/field_group/PatternFormatterTest.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel_helper\Functional\Plugin\field_group; + +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_group\Functional\FieldGroupTestTrait; + +/** + * Test the pattern field group formatter. + */ +class PatternFormatterTest extends BrowserTestBase { + + use FieldGroupTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'node', + 'text', + 'field_group', + 'oe_whitelabel_helper', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'oe_whitelabel'; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + + // Create content type. + $this->drupalCreateContentType([ + 'name' => 'Test', + 'type' => 'test', + ]); + + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ + $display = \Drupal::entityTypeManager() + ->getStorage('entity_view_display') + ->load('node.test.default'); + + // Create test fields. + $fields = [ + 'field_test_1' => 'Field 1', + 'field_test_2' => 'Field 2', + ]; + foreach ($fields as $field_name => $field_label) { + $field_storage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'node', + 'type' => 'text', + ]); + $field_storage->save(); + + $instance = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'test', + 'label' => $field_label, + ]); + $instance->save(); + + // Set the field visible on the display object. + $display->setComponent($field_name, [ + 'label' => 'above', + 'type' => 'text_default', + ]); + } + + // Save display + create node. + $display->save(); + } + + /** + * Test description list pattern formatter. + */ + public function testDescriptionListPatternFormatter() { + $assert_session = $this->assertSession(); + + $data = [ + 'weight' => '1', + 'children' => [ + 0 => 'field_test_1', + 1 => 'field_test_2', + ], + 'label' => 'Test label', + 'format_type' => 'oe_whitelabel_helper_description_list_pattern', + ]; + $group = $this->createGroup('node', 'test', 'view', 'default', $data); + field_group_group_save($group); + + $this->drupalCreateNode([ + 'type' => 'test', + 'field_test_1' => [ + ['value' => 'Content test 1'], + ], + 'field_test_2' => [ + ['value' => 'Content test 2'], + ], + ]); + + // Assert that fields are rendered using the field list horizontal pattern. + $this->drupalGet('node/1'); + $assert_session->elementsCount('css', 'dl', 1); + $assert_session->elementTextContains('css', 'dl div dt', 'Field 1'); + $assert_session->elementTextContains('css', 'dl dd:nth-child(2)', 'Content test 1'); + $assert_session->elementTextContains('css', 'dl div:nth-child(3) dt', 'Field 2'); + $assert_session->elementTextContains('css', 'dl dd:nth-child(4)', 'Content test 2'); + } + +} diff --git a/oe_whitelabel.libraries.yml b/oe_whitelabel.libraries.yml index 3243b9da6a25cdb4be55fbd11dc74a34b2f24094..1a029ba94e6e8adcef99505402f3181bf7eef4ee 100644 --- a/oe_whitelabel.libraries.yml +++ b/oe_whitelabel.libraries.yml @@ -3,3 +3,10 @@ style: css: theme: assets/css/oe_whitelabel.style.min.css: {} + +project_status: + version: VERSION + js: + resources/js/oe_whitelabel.project_status.js: {} + dependencies: + - core/jquery.once diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f5e726173dff22a539b59673b05767fa8a6a974b..efe4305824175852543ad606200c8e8b4d7b504a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,6 +7,7 @@ <env name="SIMPLETEST_BASE_URL" value="${drupal.base_url}"/> <env name="SIMPLETEST_SPARQL_DB" value="sparql://${drupal.sparql.host}:${drupal.sparql.port}/?module=sparql_entity_storage"/> <env name="SIMPLETEST_DB" value="mysql://${drupal.database.user}:${drupal.database.password}@${drupal.database.host}:${drupal.database.port}/${drupal.database.name}"/> + <env name="MINK_DRIVER_ARGS_WEBDRIVER" value='["${selenium.browser}", null, "${selenium.host}:${selenium.port}/wd/hub"]'/> </php> <testsuites> <testsuite name="OpenEuropa Whitelabel Theme"> diff --git a/resources/js/oe_whitelabel.project_status.js b/resources/js/oe_whitelabel.project_status.js new file mode 100644 index 0000000000000000000000000000000000000000..22b701e2f2be9279a4367503a925aae5b9923f89 --- /dev/null +++ b/resources/js/oe_whitelabel.project_status.js @@ -0,0 +1,61 @@ +/** + * @file + * Attaches behaviors for the project status element. + */ +(function (bootstrap, Drupal, $) { + + const colorClasses = [ + 'bg-secondary', + 'bg-info', + 'bg-dark', + ]; + + /** + * Animates the project status badge and progress bar. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Initialises the behavior. + */ + Drupal.behaviors.projectStatus = { + attach: function (context) { + $('.bcl-project-status', context).once('bcl-project-status').each(function () { + const $element = $(this); + const msBegin = $element.data('start-timestamp') * 1000; + const msEnd = $element.data('end-timestamp') * 1000; + const statusLabels = $element.data('status-labels').split('|'); + const msNow = Date.now(); + // Calculate a status id: planned = 0, ongoing = 1, closed = 2. + const status = (msNow >= msBegin) + (msNow > msEnd); + // Calculate a progress: planned = 0, ongoing = 0..1, closed = 1. + const progress01 = Math.max(0, Math.min(1, (msNow - msBegin) / (msEnd - msBegin))); + // Convert to percent: planned = 0%, ongoing = 0%..100%, closed = 100%. + // Round to 1%, to avoid overwhelming float digits in aria attributes. + const percent = Math.round(progress01 * 100); + + // Process the status label. + $('.badge', this).each(function () { + const $element = $(this); + $element.removeClass(colorClasses); + $element.addClass(colorClasses[status]); + $element.html(statusLabels[status]); + }); + + // Process the progress bar. + $('.progress-bar', this).each(function () { + const $element = $(this); + $element.removeClass(colorClasses); + $element.addClass(colorClasses[status]); + $element.css('width', percent + '%'); + $element.attr('aria-valuenow', percent); + $element.attr('aria-label', percent); + }); + + // Reveal the entire section. + $element.removeClass('d-none'); + }); + } + }; + +})(bootstrap, Drupal, jQuery); diff --git a/runner.yml.dist b/runner.yml.dist index 8d2203ba9a5587c57da232ccfe5818ddfbb03e95..4727a705c3cccce9957a21b404b4e2dabbd2f07a 100644 --- a/runner.yml.dist +++ b/runner.yml.dist @@ -20,6 +20,7 @@ drupal: - "./vendor/bin/drush en oe_authentication -y" - "./vendor/bin/drush en oe_whitelabel_multilingual -y" - "./vendor/bin/drush en oe_whitelabel_contact_forms -y" + - "./vendor/bin/drush en oe_whitelabel_extra_project -y" - "./vendor/bin/drush en oe_whitelabel_helper -y" - "./vendor/bin/drush en oe_whitelabel_search -y" - "./vendor/bin/drush en oe_whitelabel_starter_news -y" @@ -49,6 +50,12 @@ drupal: port: ${drupal.sparql.port} namespace: 'Drupal\sparql_entity_storage\Driver\Database\sparql' driver: 'sparql' + +selenium: + host: "http://selenium" + port: "4444" + browser: "chrome" + commands: drupal:site-setup: - { task: "run", command: "drupal:symlink-project" } @@ -57,6 +64,8 @@ commands: - { task: "run", command: "setup:phpunit" } setup:phpunit: - { task: "process", source: "phpunit.xml.dist", destination: "phpunit.xml" } + # Generate settings.testing.php, it will be used when running functional tests. + - { task: "process-php", type: "write", config: "drupal.settings", source: "${drupal.root}/sites/default/default.settings.php", destination: "${drupal.root}/sites/default/settings.testing.php", override: true } release: tasks: diff --git a/templates/content/field--oe-organisation--oe-acronym--oe-cx-project-stakeholder.html.twig b/templates/content/field--oe-organisation--oe-acronym--oe-cx-project-stakeholder.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..5e623ee2abf9cd12100233d77162abafc3da11c7 --- /dev/null +++ b/templates/content/field--oe-organisation--oe-acronym--oe-cx-project-stakeholder.html.twig @@ -0,0 +1,11 @@ +{# +/** + * @file + * Theme override for the Acronym field template. + * + * @see ./core/themes/stable/templates/field/field.html.twig + */ +#} +<h4 class="fw-bold mb-3"> + {{ element }} +</h4> diff --git a/templates/content/node--oe-project--full.html.twig b/templates/content/node--oe-project--full.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..5a9999f9a40e511f6e40dc8dd6e7f71362bb2e34 --- /dev/null +++ b/templates/content/node--oe-project--full.html.twig @@ -0,0 +1,71 @@ +{# +/** + * @file + * Project full display. + */ +#} +{% apply spaceless %} + +{% macro header(title, id) %} + {% set attributes = id ? create_attribute({'id': id}) %} + <h3 class="fw-bold mb-4 mt-4-5"{{ attributes }}>{{ title }}</h3> +{% endmacro %} + +{% macro field_with_header(field) %} + {% set _content %}{{ field|field_value }}{% endset %} + {% if _content|trim %} + {{ _self.header(field|field_label, field['#inpage_nav_id'] ?? null) }} + {{ _content }} + {% endif %} +{% endmacro %} + +{% set _project_details %} + {% if project_status_args is not empty %} + {{ attach_library('oe_whitelabel/project_status') }} + {% include '@oe-bcl/project-status' with project_status_args only %} + {% endif %} + {% if contributions_args is defined %} + {% include '@oe-bcl/project-status/project-contributions' with contributions_args only %} + {% endif %} + {{ content.group_project_details|filter((subgroup, key) => key|first != '#') }} +{% endset %} + +{% if _project_details|trim is not empty %} + {% set _project_details_title = 'Project details'|t %} + {% set _project_details %} + {{ _self.header(_project_details_title, 'project-details') }} + {{ _project_details }} + {% endset %} + {% set inpage_navigation_links = [{ + 'path': '#project-details', + 'label': _project_details_title, + }]|merge(inpage_navigation_links) %} +{% endif %} + +{% set inpage_navigation_content %} + {# + Reliably absorb top margin of first element. + @todo Add a utility class 'eat-margin-top' with :before style. + #} + <div class="h-0 invisible mt-n5 mb-5"></div> + {% block inpage_navigation_content %} + {{ _project_details }} + {{ _self.field_with_header(content.oe_summary) }} + {{ _self.field_with_header(content.oe_cx_objective) }} + {{ _self.field_with_header(content.oe_cx_impacts) }} + {{ _self.field_with_header(content.oe_cx_lead_contributors) }} + {{ _self.field_with_header(content.oe_project_participants) }} + {{ _self.field_with_header(content.oe_cx_achievements_and_milestone) }} + {% endblock %} +{% endset %} + +<article{{ attributes }}> + {{ pattern('inpage_navigation', { + 'title': 'Page content'|t, + 'links': inpage_navigation_links, + 'content': inpage_navigation_content, + 'full_layout': true, + }) }} +</article> + +{% endapply %} diff --git a/templates/content/node--oe-project--oe-w-content-banner.html.twig b/templates/content/node--oe-project--oe-w-content-banner.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f5c8aeb9ed6f268f0b7bdbeb66df0edf835a8165 --- /dev/null +++ b/templates/content/node--oe-project--oe-w-content-banner.html.twig @@ -0,0 +1,20 @@ +{# +/** + * @file + * Project content banner display. + */ +#} +{{ pattern('content_banner', { + 'background': 'gray', + 'title': label, + 'content': content.oe_teaser, + 'image': image, + 'attributes': create_attribute().addClass(['ps-0']), + 'badges': content.oe_subject + |field_value|default([]) + |filter(_item => _item|drupal_escape|striptags|trim is not empty) + |map(_item => { + 'label': _item, + 'rounded_pill': true, + }), +}) }} diff --git a/templates/content/node--oe-project--teaser.html.twig b/templates/content/node--oe-project--teaser.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..dc13a4979593bb2f2388e0a1bc72c20da027fc8d --- /dev/null +++ b/templates/content/node--oe-project--teaser.html.twig @@ -0,0 +1,35 @@ +{# +/** + * @file + * Template for Teaser view display of Project content type. + */ +#} +{% set _title %} + <a class="standalone" href="{{ url }}">{{ label }}</a> +{% endset %} +{% set _badges = [] %} +{% set _meta = [] %} +{% for _item in content.oe_subject|field_value %} + {% set _badges = _badges|merge([{ + label: _item, + }]) %} +{% endfor %} +{% if content.oe_project_dates|field_value is not empty %} + {% set _meta = _meta|merge([ + content.oe_project_dates|field_value, + ]) %} +{% endif %} +{% block content %} + <article{{attributes}}> + {{ pattern('card', { + variant: 'search', + title: _title, + text: content.oe_teaser|field_value, + image: (image is not empty) ? image|merge({ + path: image.src, + }) : {}, + meta: _meta, + badges: _badges, + }) }} + </article> +{% endblock %} diff --git a/tests/fixtures/example_1.jpeg b/tests/fixtures/example_1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e04cde60ca8fa8d23a3668010a40f976abb78e8b Binary files /dev/null and b/tests/fixtures/example_1.jpeg differ diff --git a/tests/src/FunctionalJavascript/ContentProjectRenderTest.php b/tests/src/FunctionalJavascript/ContentProjectRenderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7e429f8d43ceb564f43959264bb201a11fdc5620 --- /dev/null +++ b/tests/src/FunctionalJavascript/ContentProjectRenderTest.php @@ -0,0 +1,418 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\FunctionalJavascript; + +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\file\Entity\File; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\media\Entity\Media; +use Drupal\node\NodeInterface; +use Drupal\oe_content_entity\Entity\CorporateEntityInterface; +use Drupal\oe_content_entity_organisation\Entity\OrganisationInterface; +use Drupal\Tests\oe_whitelabel\PatternAssertions\ContentBannerAssert; +use Drupal\Tests\oe_whitelabel\PatternAssertions\DescriptionListAssert; +use Drupal\Tests\oe_whitelabel\PatternAssertions\InPageNavigationAssert; +use Drupal\Tests\sparql_entity_storage\Traits\SparqlConnectionTrait; +use Drupal\Tests\TestFileCreationTrait; +use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; + +/** + * Tests that our Project content type renders correctly. + */ +class ContentProjectRenderTest extends WebDriverTestBase { + + use SparqlConnectionTrait; + use TestFileCreationTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'oe_whitelabel'; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'block', + 'oe_whitelabel_extra_project', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->setUpSparql(); + + Role::load(RoleInterface::ANONYMOUS_ID) + ->grantPermission('view published skos concept entities') + ->grantPermission('view media') + ->grantPermission('view published oe_organisation') + ->save(); + + $this->config('system.date') + ->set('timezone.default', 'Europe/Brussels') + ->save(); + } + + /** + * Tests that the Project page renders correctly. + */ + public function testProjectRendering(): void { + $assert_session = $this->assertSession(); + + // Create a media entity. + // Create file and media. + $file = File::create([ + 'uri' => $this->getTestFiles('image')[0]->uri, + ]); + $file->save(); + $media = Media::create([ + 'bundle' => 'image', + 'name' => 'Image test', + 'oe_media_image' => [ + [ + 'target_id' => $file->id(), + 'alt' => 'Image test alt', + 'title' => 'Image test title', + ], + ], + ]); + $media->save(); + // Create organisations for Coordinators and Participants fields. + // Unpublished entity should not be shown. + $coordinator_organisation = $this->createStakeholderOrganisationEntity('coordinator', CorporateEntityInterface::PUBLISHED, 'oe_stakeholder'); + $participant_organisation = $this->createStakeholderOrganisationEntity('participant', CorporateEntityInterface::PUBLISHED, 'oe_cx_project_stakeholder'); + + // Create a Project node. + /** @var \Drupal\node\Entity\Node $node */ + $node = $this->getStorage('node')->create([ + 'type' => 'oe_project', + 'title' => 'Test project node', + 'oe_teaser' => 'Test project node', + 'oe_summary' => 'Summary', + 'oe_featured_media' => [ + 'target_id' => (int) $media->id(), + 'caption' => 'Caption project_featured_media', + ], + 'oe_project_dates' => [ + 'value' => '2020-05-10', + 'end_value' => '2025-05-15', + ], + 'oe_project_budget' => '200', + 'oe_project_budget_eu' => '70', + 'oe_project_website' => [ + [ + 'uri' => 'http://example.com', + 'title' => 'Example website', + ], + ], + 'oe_reference_code' => 'Project reference', + 'oe_subject' => 'http://data.europa.eu/uxp/1386', + 'oe_project_funding_programme' => 'http://publications.europa.eu/resource/authority/eu-programme/AFIS2020', + 'oe_project_coordinators' => [$coordinator_organisation], + 'oe_project_participants' => [$participant_organisation], + 'oe_cx_objective' => 'Objective', + 'oe_cx_impacts' => 'Impacts', + 'oe_cx_achievements_and_milestone' => 'Achievements and milestone', + 'uid' => 0, + 'status' => 1, + ]); + $node->save(); + $this->drupalGet($node->toUrl()); + + // Assert content banner. + $content_banner = $assert_session->elementExists('css', '.bcl-content-banner'); + $assert = new ContentBannerAssert(); + $assert->assertPattern([ + 'image' => [ + 'alt' => 'Image test alt', + 'src' => 'image-test.png', + ], + 'badges' => ['wood industry'], + 'title' => 'Test project node', + 'description' => 'Test project node', + ], $content_banner->getOuterHtml()); + + // Assert in-page navigation. + $inpage_nav = $this->assertSession()->elementExists('css', 'nav.bcl-inpage-navigation'); + $inpage_nav_assert = new InPageNavigationAssert(); + $inpage_nav_assert->assertPattern([ + 'title' => 'Page content', + 'links' => [ + [ + 'label' => 'Project details', + 'href' => '#project-details', + ], + [ + 'label' => 'Summary', + 'href' => '#oe-project-oe-summary', + ], + [ + 'label' => 'Objective', + 'href' => '#oe-project-oe-cx-objective', + ], + [ + 'label' => 'Impacts', + 'href' => '#oe-project-oe-cx-impacts', + ], + [ + 'label' => 'Participants', + 'href' => '#oe-project-oe-project-participants', + ], + [ + 'label' => 'Achievements and milestones', + 'href' => '#oe-project-oe-cx-achievements-and-milestone', + ], + ], + ], $inpage_nav->getOuterHtml()); + + // Select the content column next to the in-page navigation. + $project_content = $assert_session->elementExists('css', '.col-md-9'); + + $this->assertProjectStatusTimestampsAsDateStrings('2020-05-10 00:00:00', '2025-05-16 00:00:00'); + $this->assertProjectStatusVisible(); + + $contributions_chart = $assert_session->elementExists('css', '.bcl-project-contributions .circular-progress'); + // The correct value would be 35, but it is rounded up to 40, because no + // utility classes are available for smaller increments. + $this->assertSame('40', $contributions_chart->getAttribute('data-percentage')); + + // Select the description blocks inside the Project details. + $description_lists = $project_content->findAll('css', '.grid-3-9'); + $this->assertCount(4, $description_lists); + + $description_list_assert = new DescriptionListAssert(); + + // Assert budget list group. + $description_list_assert->assertPattern([ + 'items' => [ + [ + 'term' => 'Overall budget', + 'definition' => '€200,00', + ], + [ + 'term' => 'EU contribution', + 'definition' => '€70,00', + ], + ], + ], $description_lists[0]->getHtml()); + + // Assert details list group. + $description_list_assert->assertPattern([ + 'items' => [ + [ + 'term' => 'Website', + 'definition' => 'Example website', + ], + [ + 'term' => 'Funding programme', + 'definition' => 'Anti Fraud Information System (AFIS)', + ], + [ + 'term' => 'Reference', + 'definition' => 'Project reference', + ], + ], + ], $description_lists[1]->getHtml()); + + // Assert coordinators list group. + $description_list_assert->assertPattern([ + 'items' => [ + [ + 'term' => 'Coordinators', + 'definition' => 'coordinator', + ], + ], + ], $description_lists[2]->getHtml()); + + // Assert participants list group. + $description_list_assert->assertPattern([ + 'items' => [ + [ + 'term' => 'Name', + 'definition' => 'participant', + ], + [ + 'term' => 'Address', + 'definition' => 'Belgium', + ], + [ + 'term' => 'Contribution to the budget', + 'definition' => '€22,30', + ], + ], + ], $description_lists[3]->getHtml()); + + // Set a project period that is fully in the past. + $this->setProjectDateRange($node, '2019-03-07', '2019-03-21'); + $node->save(); + $this->drupalGet($node->toUrl()); + + $this->assertProjectStatusTimestampsAsDateStrings('2019-03-07 00:00:00', '2019-03-22 00:00:00'); + $this->assertProjectStatusVisible(); + $this->assertProjectStatus('bg-dark', 'Closed'); + $this->assertProjectProgress(100); + + // Set a project period that is ongoing. + $this->setProjectDateRange($node, '-5 day', '+15 days'); + $node->save(); + $this->drupalGet($node->toUrl()); + + $this->assertProjectStatusVisible(); + $this->assertProjectStatus('bg-info', 'Ongoing'); + $this->assertProjectProgress(15, 35); + + // Set a project period that is fully in the future. + $this->setProjectDateRange($node, '+5 days', '+12 days'); + $node->save(); + $this->drupalGet($node->toUrl()); + + $this->assertProjectStatusVisible(); + $this->assertProjectStatus('bg-secondary', 'Planned'); + $this->assertProjectProgress(0); + } + + /** + * Creates a stakeholder organisation entity. + * + * @param string $name + * Name of the entity. Is used as a parameter for test data. + * @param int $status + * Entity status. 1 - published, 0 - unpublished. + * @param string $bundle + * Bundle name used in the entity. + * + * @return \Drupal\oe_content_entity_organisation\Entity\OrganisationInterface + * Organisation entity. + */ + protected function createStakeholderOrganisationEntity(string $name, int $status, string $bundle): OrganisationInterface { + $organisation = $this->getStorage('oe_organisation')->create([ + 'bundle' => $bundle, + 'name' => $name, + 'oe_acronym' => "Acronym $name", + 'oe_address' => [ + 'country_code' => 'BE', + ], + 'oe_cx_contribution_budget' => '22.3', + 'status' => $status, + ]); + $organisation->save(); + + return $organisation; + } + + /** + * Gets the entity type's storage. + * + * @param string $entity_type_id + * The entity type ID to get a storage for. + * + * @return \Drupal\Core\Entity\EntityStorageInterface + * The entity type's storage. + */ + protected function getStorage(string $entity_type_id): EntityStorageInterface { + return \Drupal::entityTypeManager()->getStorage($entity_type_id); + } + + /** + * Updates the project date, saves the node, and refreshes the page. + * + * @param \Drupal\node\NodeInterface $node + * Node to update. + * @param string $begin + * Start date string. + * @param string $end + * End date string. + */ + protected function setProjectDateRange(NodeInterface $node, string $begin, string $end): void { + $node->oe_project_dates = [ + 'value' => (new DrupalDateTime($begin, 'Europe/Brussels'))->format('Y-m-d'), + 'end_value' => (new DrupalDateTime($end, 'Europe/Brussels'))->format('Y-m-d'), + ]; + } + + /** + * Asserts that the d-none class has been removed from project status. + */ + protected function assertProjectStatusVisible(): void { + $status_section = $this->assertSession()->elementExists('css', '.bcl-project-status'); + $this->assertFalse($status_section->hasClass('d-none')); + } + + /** + * Asserts 'start' and 'end' timestamp in project status section. + * + * @param string $begin + * Expected start date string as 'Y-m-d H:i:s'. + * @param string $end + * Expected end date string as 'Y-m-d H:i:s'. + */ + protected function assertProjectStatusTimestampsAsDateStrings(string $begin, string $end): void { + $status_section = $this->assertSession()->elementExists('css', '.bcl-project-status'); + $t_begin = (int) $status_section->getAttribute('data-start-timestamp'); + $t_end = (int) $status_section->getAttribute('data-end-timestamp'); + $this->assertTimestampAsDateString($begin, $t_begin, 'Europe/Brussels'); + $this->assertTimestampAsDateString($end, $t_end, 'Europe/Brussels'); + } + + /** + * Asserts a timestamp matches a date string in a given timezone. + * + * @param string $expected + * Expected date string as 'Y-m-d H:i:s'. + * @param int $timestamp + * Actual timestamp. + * @param string $timezone + * Timezone for the conversion. + */ + protected function assertTimestampAsDateString(string $expected, int $timestamp, string $timezone): void { + $this->assertSame( + $expected, + DrupalDateTime::createFromTimestamp($timestamp, $timezone) + ->format('Y-m-d H:i:s'), + ); + } + + /** + * Asserts the state of the status badge and the color of the progress bar. + * + * @param string $color_class + * Expected color class. + * @param string $status_text + * Expected status text. + */ + protected function assertProjectStatus(string $color_class, string $status_text): void { + $status_badge = $this->assertSession()->elementExists('css', '.bcl-project-status .badge'); + $this->assertTrue($status_badge->hasClass($color_class)); + $this->assertSame($status_text, $status_badge->getHtml()); + $progress_bar = $this->assertSession()->elementExists('css', '.bcl-project-status .progress-bar'); + $this->assertTrue($progress_bar->hasClass($color_class)); + } + + /** + * Asserts the value of the progress bar. + * + * @param int $min + * Minimum progress in percent. + * @param int|null $max + * Maximum progress in percent. + */ + protected function assertProjectProgress(int $min, int $max = NULL): void { + $progress_bar = $this->assertSession()->elementExists('css', '.bcl-project-status .progress-bar'); + $progress_string = $progress_bar->getAttribute('aria-valuenow'); + $this->assertStringContainsString("width: $progress_string%", $progress_bar->getAttribute('style')); + if ($max === NULL) { + $this->assertSame((string) $min, $progress_string); + } + else { + $this->assertGreaterThanOrEqual($min, (float) $progress_string); + $this->assertLessThanOrEqual($max, (float) $progress_string); + } + } + +} diff --git a/tests/src/Kernel/AbstractKernelTestBase.php b/tests/src/Kernel/AbstractKernelTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..25b39ba842e0c5d51e557d01050f3ba7c5b5e3b2 --- /dev/null +++ b/tests/src/Kernel/AbstractKernelTestBase.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\Kernel; + +use Drupal\Tests\oe_bootstrap_theme\Kernel\AbstractKernelTestBase as BootstrapKernelTestBase; +use Drupal\Tests\oe_bootstrap_theme\Kernel\Traits\RenderTrait; + +/** + * Base class for theme's kernel tests. + */ +abstract class AbstractKernelTestBase extends BootstrapKernelTestBase { + + use RenderTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'oe_whitelabel_helper', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig([ + 'oe_whitelabel_helper', + ]); + + $this->container->get('theme_installer')->install(['oe_whitelabel']); + $this->config('system.theme')->set('default', 'oe_whitelabel')->save(); + $this->container->set('theme.registry', NULL); + } + +} diff --git a/tests/src/Kernel/ContentRenderTestBase.php b/tests/src/Kernel/ContentRenderTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..bee23bdea495b92d213abcae1e3934d25ca43ede --- /dev/null +++ b/tests/src/Kernel/ContentRenderTestBase.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\Kernel; + +use Drupal\Tests\sparql_entity_storage\Traits\SparqlConnectionTrait; +use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; + +/** + * Base class for testing the content being rendered. + */ +abstract class ContentRenderTestBase extends AbstractKernelTestBase { + + use SparqlConnectionTrait; + + /** + * The node view builder. + * + * @var \Drupal\Core\Entity\EntityViewBuilderInterface + */ + protected $nodeViewBuilder; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'address', + 'composite_reference', + 'datetime', + 'entity_reference', + 'entity_reference_revisions', + 'field', + 'field_group', + 'file', + 'file_link', + 'filter', + 'inline_entity_form', + 'link', + 'media', + 'node', + 'oe_content', + 'oe_content_departments_field', + 'oe_content_documents_field', + 'oe_content_entity', + 'oe_content_entity_contact', + 'oe_content_entity_organisation', + 'oe_content_reference_code_field', + 'oe_media', + 'options', + 'rdf_skos', + 'sparql_entity_storage', + 'text', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->setUpSparql(); + + $this->installEntitySchema('node'); + $this->installSchema('file', 'file_usage'); + $this->installEntitySchema('media'); + $this->installEntitySchema('file'); + + $this->container->get('module_handler')->loadInclude('oe_content_documents_field', 'install'); + oe_content_documents_field_install(FALSE); + + $this->installConfig([ + 'node', + 'filter', + 'oe_media', + 'oe_content', + 'oe_content_entity', + 'oe_content_entity_organisation', + 'oe_content_departments_field', + 'oe_content_reference_code_field', + ]); + + Role::load(RoleInterface::ANONYMOUS_ID) + ->grantPermission('view published skos concept entities') + ->grantPermission('view media') + ->save(); + + module_load_include('install', 'oe_content'); + oe_content_install(FALSE); + + $this->nodeViewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('node'); + } + +} diff --git a/tests/src/Kernel/ProjectRenderTest.php b/tests/src/Kernel/ProjectRenderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..17d06e3b93750c356f43fd6df6fd261abba86d3b --- /dev/null +++ b/tests/src/Kernel/ProjectRenderTest.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\Kernel; + +use Drupal\media\Entity\Media; +use Drupal\node\Entity\Node; +use Drupal\Tests\oe_whitelabel\PatternAssertions\CardAssert; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests the rendering of the teaser view mode of Project content type. + */ +class ProjectRenderTest extends ContentRenderTestBase { + + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'datetime_range', + 'image', + 'oe_content_featured_media_field', + 'oe_content_project', + 'oe_content_extra', + 'oe_content_extra_project', + 'oe_whitelabel_extra_project', + 'system', + 'twig_field_value', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig([ + 'oe_content_featured_media_field', + 'oe_content_project', + 'oe_content_extra', + 'oe_content_extra_project', + 'oe_whitelabel_extra_project', + ]); + + module_load_include('install', 'oe_whitelabel_extra_project'); + oe_whitelabel_extra_project_install(FALSE); + } + + /** + * Test a project being rendered as a teaser. + */ + public function testProjectTeaser(): void { + $file = file_save_data(file_get_contents(drupal_get_path('theme', 'oe_whitelabel') . '/tests/fixtures/example_1.jpeg'), 'public://example_1.jpeg'); + $file->setPermanent(); + $file->save(); + + $media = Media::create([ + 'bundle' => 'image', + 'name' => 'test image', + 'oe_media_image' => [ + 'target_id' => $file->id(), + 'alt' => 'Alternative text', + 'caption' => 'Caption', + ], + ]); + $media->save(); + + $values = [ + 'type' => 'oe_project', + 'title' => 'Project 1', + 'oe_subject' => 'http://data.europa.eu/uxp/1005', + 'oe_teaser' => 'The teaser text', + 'oe_featured_media' => [ + [ + 'target_id' => $media->id(), + 'caption' => 'Caption text', + 'alt' => 'Alternative text', + ], + ], + 'oe_project_dates' => [ + 'value' => '2020-05-10', + 'end_value' => '2025-05-15', + ], + 'status' => 1, + ]; + + $node = Node::create($values); + $node->save(); + + $build = $this->nodeViewBuilder->view($node, 'teaser'); + $html = $this->renderRoot($build); + + $assert = new CardAssert(); + + $expected_values = [ + 'title' => 'Project 1', + 'url' => '/node/1', + 'description' => 'The teaser text', + 'badges' => ['EU financing'], + 'image' => [ + 'src' => 'example_1.jpeg', + 'alt' => 'Alternative text', + ], + 'content' => [ + '10 May 2020', + '15 May 2025', + ], + ]; + $assert->assertPattern($expected_values, $html); + $assert->assertVariant('search', $html); + } + +} diff --git a/tests/src/PatternAssertions/BasePatternAssert.php b/tests/src/PatternAssertions/BasePatternAssert.php new file mode 100644 index 0000000000000000000000000000000000000000..4bedfa7b1d144e42ad8aada691bef352b0367a2d --- /dev/null +++ b/tests/src/PatternAssertions/BasePatternAssert.php @@ -0,0 +1,221 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\PatternAssertions; + +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\Exception; +use Symfony\Component\DomCrawler\Crawler; + +/** + * Base class for asserting patterns. + */ +abstract class BasePatternAssert extends Assert implements PatternAssertInterface { + + /** + * Method that returns the assertions to be run by a particular pattern. + * + * Assertions extending this class need to return an array containing the + * assertions to be run for every possible value that can be expected. + * + * @param string $variant + * The variant name that is being checked. + * + * @return array + * An array containing the assertions to be run. + */ + abstract protected function getAssertions(string $variant): array; + + /** + * Method that asserts the base elements of a rendered pattern. + * + * @param string $html + * The rendered pattern. + * @param string $variant + * The variant being asserted. + */ + abstract protected function assertBaseElements(string $html, string $variant): void; + + /** + * Returns the variant of the provided rendered pattern. + * + * @param string $html + * The rendered pattern. + * + * @return string + * The variant of the rendered pattern. + */ + protected function getPatternVariant(string $html): string { + return 'default'; + } + + /** + * {@inheritdoc} + */ + public function assertPattern(array $expected, string $html): void { + $variant = $this->getPatternVariant($html); + $this->assertBaseElements($html, $variant); + $assertion_map = $this->getAssertions($variant); + $crawler = new Crawler($html); + foreach ($expected as $name => $expected_value) { + if (!array_key_exists($name, $assertion_map)) { + $reflection = new \ReflectionClass($this); + throw new Exception(sprintf('"%s" does not provide any assertion for "%s".', $reflection->getName(), $name)); + } + if (is_array($assertion_map[$name]) && is_callable($assertion_map[$name][0])) { + $callback = array_shift($assertion_map[$name]); + array_unshift($assertion_map[$name], $expected_value); + $assertion_map[$name][] = $crawler; + call_user_func_array($callback, $assertion_map[$name]); + } + } + } + + /** + * {@inheritdoc} + */ + public function assertVariant(string $variant, string $html): void { + self::assertEquals($variant, $this->getPatternVariant($html)); + } + + /** + * Asserts the value of an attribute of a particular element. + * + * @param string|null $expected + * The expected value. + * @param string $selector + * The CSS selector to find the element. + * @param string $attribute + * The name of the attribute to check. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertElementAttribute($expected, string $selector, string $attribute, Crawler $crawler): void { + if (is_null($expected)) { + $this->assertElementNotExists($selector, $crawler); + return; + } + $this->assertElementExists($selector, $crawler); + $element = $crawler->filter($selector); + self::assertEquals($expected, $element->attr($attribute)); + } + + /** + * Asserts the text of a particular element. + * + * @param string|null $expected + * The expected value. + * @param string $selector + * The CSS selector to find the element. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertElementText($expected, string $selector, Crawler $crawler): void { + if (is_null($expected)) { + $this->assertElementNotExists($selector, $crawler); + return; + } + $this->assertElementExists($selector, $crawler); + $element = $crawler->filter($selector); + $actual = trim($element->text()); + self::assertEquals($expected, $actual, \sprintf( + 'Expected text value "%s" is not equal to the actual value "%s" found in the selector "%s".', + $expected, $actual, $selector + )); + } + + /** + * Asserts the rendered html of a particular element. + * + * @param string|null $expected + * The expected value. + * @param string $selector + * The CSS selector to find the element. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertElementHtml($expected, string $selector, Crawler $crawler): void { + if (is_null($expected)) { + $this->assertElementNotExists($selector, $crawler); + return; + } + $this->assertElementExists($selector, $crawler); + $element = $crawler->filter($selector); + self::assertEquals($expected, $element->html()); + } + + /** + * Asserts that an element is present. + * + * @param string $selector + * The CSS selector to find the element. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertElementExists(string $selector, Crawler $crawler): void { + $element = $crawler->filter($selector); + self::assertCount(1, $element, \sprintf( + 'Element with selector "%s" not found in the provided html.', + $selector + )); + } + + /** + * Asserts that an element is not present. + * + * @param string $selector + * The CSS selector to find the element. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertElementNotExists(string $selector, Crawler $crawler): void { + $element = $crawler->filter($selector); + self::assertCount(0, $element, \sprintf( + 'Element with selector "%s" was found in the provided html.', + $selector + )); + } + + /** + * Asserts the image of the pattern. + * + * @param array|null $expected_image + * The expected image. + * @param string $selector + * The CSS selector to find the element. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertImage(?array $expected_image, string $selector, Crawler $crawler): void { + if (is_null($expected_image)) { + $this->assertElementNotExists($selector, $crawler); + return; + } + $this->assertElementExists($selector, $crawler); + $element = $crawler->filter($selector); + self::assertEquals($expected_image['alt'], $element->attr('alt')); + self::assertStringContainsString($expected_image['src'], $element->attr('src')); + } + + /** + * Asserts the badges items of the pattern. + * + * @param array $badges + * The expected badges item values. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertBadgesElements(array $badges, Crawler $crawler): void { + if (empty($badges)) { + $this->assertElementNotExists('.badge', $crawler); + return; + } + $badges_items = $crawler->filter('.mt-2-5'); + self::assertCount(count($badges), $badges_items); + foreach ($badges as $index => $badge) { + self::assertEquals($badge, trim($badges_items->eq($index)->text())); + } + } + +} diff --git a/tests/src/PatternAssertions/CardAssert.php b/tests/src/PatternAssertions/CardAssert.php new file mode 100644 index 0000000000000000000000000000000000000000..00afdb6cb0f3052d70289b6f7ac7660195f140a0 --- /dev/null +++ b/tests/src/PatternAssertions/CardAssert.php @@ -0,0 +1,133 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\PatternAssertions; + +use Symfony\Component\DomCrawler\Crawler; + +/** + * Assertions for the card pattern. + * + * @see ./templates/patterns/card/card.ui_patterns.yml + */ +class CardAssert extends BasePatternAssert { + + /** + * {@inheritdoc} + */ + protected function getAssertions($variant): array { + return [ + 'title' => [ + [$this, 'assertElementText'], + '.card-title a span', + ], + 'url' => [ + [$this, 'assertElementAttribute'], + '.card-title a', + 'href', + ], + 'image' => [ + [$this, 'assertCardImage'], + $variant, + ], + 'description' => [ + [$this, 'assertElementText'], + '.card-text', + ], + 'badges' => [ + [$this, 'assertBadgesElements'], + ], + 'content' => [ + [$this, 'assertContent'], + $variant, + ], + ]; + } + + /** + * Asserts the image of a card. + * + * @param array|null $expected_image + * The expected image values. + * @param string $variant + * The variant of the pattern being checked. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertCardImage($expected_image, string $variant, Crawler $crawler): void { + if ($variant == 'search') { + $image_div = $crawler->filter('.row .col-md-3.mw-listing-img img.card-img-top'); + self::assertEquals($expected_image['alt'], $image_div->attr('alt')); + self::assertStringContainsString($expected_image['src'], $image_div->attr('src')); + } + else { + $image_div = $crawler->filter('div.card img'); + self::assertEquals($expected_image['alt'], $image_div->attr('alt')); + self::assertStringContainsString($expected_image['src'], $image_div->attr('src')); + } + } + + /** + * Asserts the content of a card. + * + * @param array $expected_items + * The expected item values. + * @param string $variant + * The variant of the pattern being checked. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertContent(array $expected_items, string $variant, Crawler $crawler): void { + // There's no wrapping element in content that can be targeted, + // so we are checking that the expected items are present. + foreach ($expected_items as $expected_item) { + if ($variant == 'search') { + self::assertStringContainsString($expected_item, $crawler->filter('.row .col-md-9')->html()); + } + else { + self::assertStringContainsString($expected_item, $crawler->html()); + } + } + } + + /** + * {@inheritdoc} + */ + protected function assertBaseElements(string $html, string $variant): void { + $crawler = new Crawler($html); + $card = $crawler->filter($this->getBaseItemClass($variant)); + self::assertCount(1, $card); + } + + /** + * Returns the base CSS selector for a list item depending on the variant. + * + * @param string $variant + * The variant being checked. + * + * @return string + * The base selector for the variant. + */ + protected function getBaseItemClass(string $variant): string { + switch ($variant) { + case 'search': + return 'div.listing-item.card'; + + default: + return 'div.card'; + } + } + + /** + * {@inheritdoc} + */ + protected function getPatternVariant(string $html): string { + $crawler = new Crawler($html); + if ($crawler->filter('div.listing-item')->count() > 0) { + return 'search'; + } + return 'default'; + } + +} diff --git a/tests/src/PatternAssertions/ContentBannerAssert.php b/tests/src/PatternAssertions/ContentBannerAssert.php new file mode 100644 index 0000000000000000000000000000000000000000..af68fe0e2639c9b34f0abb9845e5ce50fff2f480 --- /dev/null +++ b/tests/src/PatternAssertions/ContentBannerAssert.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\PatternAssertions; + +use Symfony\Component\DomCrawler\Crawler; + +/** + * Assertions for the page header pattern. + */ +class ContentBannerAssert extends BasePatternAssert { + + /** + * {@inheritdoc} + */ + protected function getAssertions(string $variant): array { + return [ + 'image' => [ + [$this, 'assertImage'], + '.card-img-top', + ], + 'badges' => [ + [$this, 'assertBadgesElements'], + ], + 'title' => [ + [$this, 'assertElementText'], + '.card-title', + ], + 'description' => [ + [$this, 'assertElementText'], + '.card-body .mt-4', + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function assertBaseElements(string $html, string $variant): void { + $crawler = new Crawler($html); + $page_header = $crawler->filter('.bcl-content-banner'); + self::assertCount(1, $page_header); + } + +} diff --git a/tests/src/PatternAssertions/DescriptionListAssert.php b/tests/src/PatternAssertions/DescriptionListAssert.php new file mode 100644 index 0000000000000000000000000000000000000000..32b228797b8a3bf0c2728ddc053baed7b3b237d0 --- /dev/null +++ b/tests/src/PatternAssertions/DescriptionListAssert.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\PatternAssertions; + +use Symfony\Component\DomCrawler\Crawler; + +/** + * Assertions for the field list pattern. + * + * @see ./templates/patterns/field_list/field_list.ui_patterns.yml + */ +class DescriptionListAssert extends BasePatternAssert { + + /** + * {@inheritdoc} + */ + protected function getAssertions(string $variant): array { + return [ + 'items' => [ + [$this, 'assertItems'], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function assertBaseElements(string $html, string $variant): void { + $crawler = new Crawler($html); + $field_list_container = $crawler->filter('body'); + self::assertCount(1, $field_list_container); + } + + /** + * Asserts the items of the pattern. + * + * @param array $expected_items + * The expected item values. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertItems(array $expected_items, Crawler $crawler): void { + // Assert all labels are correct. + $expected_labels = array_column($expected_items, 'term'); + $label_items = $crawler->filter('dt'); + self::assertCount(count($expected_labels), $label_items); + foreach ($expected_labels as $index => $expected_label) { + self::assertEquals($expected_label, trim($label_items->eq($index)->text())); + } + + // Assert all values are correct. + $expected_values = array_column($expected_items, 'definition'); + $value_items = $crawler->filter('dd'); + self::assertCount(count($expected_labels), $value_items); + foreach ($expected_values as $index => $expected_value) { + self::assertEquals($expected_value, trim($value_items->eq($index)->text())); + } + } + +} diff --git a/tests/src/PatternAssertions/InPageNavigationAssert.php b/tests/src/PatternAssertions/InPageNavigationAssert.php new file mode 100644 index 0000000000000000000000000000000000000000..b2d9bdf9be26042549046f5412dc692e210eb391 --- /dev/null +++ b/tests/src/PatternAssertions/InPageNavigationAssert.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\PatternAssertions; + +use Symfony\Component\DomCrawler\Crawler; + +/** + * Assertions for in-page-navigation. + */ +class InPageNavigationAssert extends BasePatternAssert { + + /** + * {@inheritdoc} + */ + protected function getAssertions(string $variant): array { + return [ + 'title' => [ + [$this, 'assertElementText'], + 'h5', + ], + 'links' => [ + [$this, 'assertList'], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function assertBaseElements(string $html, string $variant): void { + $crawler = new Crawler($html); + $page_header = $crawler->filter('body'); + self::assertCount(1, $page_header); + } + + /** + * Asserts the in-page-navigation links list. + * + * @param array|null $expected + * The expected description values. + * @param \Symfony\Component\DomCrawler\Crawler $crawler + * The DomCrawler where to check the element. + */ + protected function assertList($expected, Crawler $crawler): void { + $this->assertElementExists('ul.nav-pills', $crawler); + + $actual = []; + $crawler->filter('ul.nav-pills a.nav-link')->each(function (Crawler $node) use (&$actual) { + $actual[] = [ + 'label' => $node->text(), + 'href' => $node->attr('href'), + ]; + }); + + self::assertEquals($expected, $actual); + } + +} diff --git a/tests/src/PatternAssertions/PatternAssertInterface.php b/tests/src/PatternAssertions/PatternAssertInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..293eb8ffe66089edbef53a7fcf9d13817e4849ce --- /dev/null +++ b/tests/src/PatternAssertions/PatternAssertInterface.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\oe_whitelabel\PatternAssertions; + +/** + * Interface implemented by all pattern assertion objects. + */ +interface PatternAssertInterface { + + /** + * Asserts that a rendered pattern is correct. + * + * @param array $expected + * An array of expected values, keyed by field name. + * @param string $html + * The rendered pattern. + */ + public function assertPattern(array $expected, string $html): void; + + /** + * Asserts that a rendered pattern uses a variant. + * + * @param string $variant + * The variant to check for. + * @param string $html + * The rendered pattern. + */ + public function assertVariant(string $variant, string $html): void; + +}