diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8f39a873..d9e6ac435 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,8 @@ jobs: storybookBaseDir: packages/ui onlyChanged: true exitOnceUploaded: true + externals: | + static/stories/webforms/** if: ${{ steps.chromatic-check.outputs.available == 'true' }} - name: Deploy storybook to netlify diff --git a/.github/workflows/test_without_turbo_cache.yml b/.github/workflows/test_without_turbo_cache.yml index 837d24c68..c247a595e 100644 --- a/.github/workflows/test_without_turbo_cache.yml +++ b/.github/workflows/test_without_turbo_cache.yml @@ -3,11 +3,14 @@ on: workflow_dispatch: jobs: - test: name: Test runs-on: ubuntu-20.04 steps: + - name: Init check + if: ${{ github.repository != 'AmazeeLabs/silverback-template'}} + run: echo 'Please run the INIT script. See the root README.md for instructions.' && false + - name: Checkout uses: actions/checkout@v3 with: @@ -16,8 +19,18 @@ jobs: - name: Setup uses: ./.github/actions/setup + - name: TurboRepo local server + uses: felixmosh/turborepo-gh-artifacts@v2 + with: + server-token: 'local' + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Test - run: pnpm turbo:test + run: pnpm turbo:test:force + env: + TURBO_API: 'http://127.0.0.1:9080' + TURBO_TOKEN: 'local' + TURBO_TEAM: 'local' - name: Upload Playwright report uses: actions/upload-artifact@v3 diff --git a/apps/cms/composer.json b/apps/cms/composer.json index bb6feeb43..50427ad02 100644 --- a/apps/cms/composer.json +++ b/apps/cms/composer.json @@ -100,6 +100,9 @@ }, "amazeelabs/silverback_gatsby": { "Autosave preview": "./patches/fetch-entity.patch" + }, + "drupal/gutenberg": { + "Gutenberg enabled hook": "https://www.drupal.org/files/issues/2024-05-07/gutenberg_enabled_hook_3445677-2.patch" } }, "patchLevel": { diff --git a/apps/cms/config/sync/core.entity_form_display.node.page.split.yml b/apps/cms/config/sync/core.entity_form_display.node.page.split.yml new file mode 100644 index 000000000..7c4e46ca1 --- /dev/null +++ b/apps/cms/config/sync/core.entity_form_display.node.page.split.yml @@ -0,0 +1,103 @@ +uuid: a39d0011-2ebf-4b18-a9f0-8b5aeff5b9aa +langcode: en +status: true +dependencies: + config: + - core.entity_form_mode.node.split + - field.field.node.page.body + - field.field.node.page.field_metatags + - node.type.page + - workflows.workflow.basic + module: + - content_moderation + - metatag + - path +id: node.page.split +targetEntityType: node +bundle: page +mode: split +content: + created: + type: datetime_timestamp + weight: 3 + region: content + settings: { } + third_party_settings: { } + field_metatags: + type: metatag_firehose + weight: 11 + region: content + settings: + sidebar: true + use_details: true + third_party_settings: { } + langcode: + type: language_select + weight: 1 + region: content + settings: + include_locked: true + third_party_settings: { } + moderation_state: + type: moderation_state_default + weight: 9 + region: content + settings: { } + third_party_settings: { } + path: + type: path + weight: 7 + region: content + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + weight: 5 + region: content + settings: + display_label: true + third_party_settings: { } + status: + type: boolean_checkbox + weight: 10 + region: content + settings: + display_label: true + third_party_settings: { } + sticky: + type: boolean_checkbox + weight: 6 + 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: 4 + region: content + settings: { } + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 2 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } + url_redirects: + weight: 8 + region: content + settings: { } + third_party_settings: { } +hidden: + body: true diff --git a/apps/cms/config/sync/core.entity_form_mode.node.split.yml b/apps/cms/config/sync/core.entity_form_mode.node.split.yml new file mode 100644 index 000000000..8e665c318 --- /dev/null +++ b/apps/cms/config/sync/core.entity_form_mode.node.split.yml @@ -0,0 +1,11 @@ +uuid: 5f26940d-fd06-41e0-8e59-9dfcdc9c86df +langcode: en +status: true +dependencies: + module: + - node +id: node.split +label: Split +description: '' +targetEntityType: node +cache: true diff --git a/apps/cms/config/sync/core.extension.yml b/apps/cms/config/sync/core.extension.yml index c32cf22ef..84b5d6503 100644 --- a/apps/cms/config/sync/core.extension.yml +++ b/apps/cms/config/sync/core.extension.yml @@ -22,6 +22,7 @@ module: dropzonejs: 0 dynamic_page_cache: 0 editor: 0 + entity_create_split: 0 entity_usage: 0 environment_indicator: 0 field: 0 diff --git a/apps/cms/patches/.gitkeep b/apps/cms/patches/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/website/publisher.config.ts b/apps/website/publisher.config.ts index abaf98f01..6c5d95797 100644 --- a/apps/website/publisher.config.ts +++ b/apps/website/publisher.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ // cannot report it. // Workaround: Do a double build on the first build. 'if test -d public; then echo "Single build" && pnpm build:gatsby; else echo "Double build" && pnpm build:gatsby && pnpm build:gatsby; fi' - : 'pnpm build:gatsby', + : 'DRUPAL_EXTERNAL_URL=http://127.0.0.1:8888 pnpm build:gatsby', outputTimeout: 1000 * 60 * 10, }, clean: 'pnpm clean', diff --git a/package.json b/package.json index e4b531976..12321130c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:format:workspaces": "pnpm --workspace-concurrency=1 -r exec prettier '**/*.{js,cjs,mjs,ts,jsx,tsx,gql,graphql,graphqls,md,mdx,json,htm,html}' --ignore-path='./.gitignore'", "turbo:local": "if [ -z $CI ]; then echo $(date)$RANDOM > apps/cms/turbo-seed.txt; fi", "turbo:test": "pnpm turbo:local && pnpm tb test:unit --no-daemon --go-fallback --output-logs=new-only && pnpm tb test:integration --no-daemon --go-fallback --output-logs=new-only --concurrency=1", + "turbo:test:force": "pnpm tb test:unit --no-daemon --go-fallback --output-logs=new-only --force && pnpm tb test:integration --no-daemon --go-fallback --output-logs=new-only --concurrency=1 --force", "turbo:test:quick": "pnpm turbo:local && pnpm tb test:unit --no-daemon --go-fallback --output-logs=new-only", "turbo:prep": "pnpm turbo:local && pnpm tb prep --no-daemon --go-fallback --output-logs=new-only", "turbo:prep:force": "rm -f apps/cms/web/sites/default/files/.sqlite && pnpm tb prep --no-daemon --go-fallback --force", diff --git a/packages/drupal/entity_create_split/README.md b/packages/drupal/entity_create_split/README.md new file mode 100644 index 000000000..12a79a2d2 --- /dev/null +++ b/packages/drupal/entity_create_split/README.md @@ -0,0 +1,13 @@ +# Entity Create Split + +A Drupal module which exposes routes to split an entity create form into two parts: +- on the first step, only the required fields are presented. After submitting the form, a entity is already created +- the second step is actually just the entity edit form, containing all the other optional form fields. + +To enable this feature, you must create a form mode with the machine name "split" and enable it on the bundle for which you want to have this feature. + +## Special case for the Gutenberg editor + +The Gutenberg editor does a lot of alterations on the create form. For this reason, it is better that the form alter hook of the gutenberg module to not run at all. This is not easy possible, so right now the easiest approach is to just patch the module with the patch from https://www.drupal.org/project/gutenberg/issues/3445677/ + +The functionality should also work without the patch, but the initial form will not look that nice. diff --git a/packages/drupal/entity_create_split/entity_create_split.info.yml b/packages/drupal/entity_create_split/entity_create_split.info.yml new file mode 100644 index 000000000..b8d2ad45e --- /dev/null +++ b/packages/drupal/entity_create_split/entity_create_split.info.yml @@ -0,0 +1,5 @@ +name: Entity Create Split +type: module +description: 'Provides a route for splitting the entity creation in two steps: first for the mandatory fields and a second one, which is actually the edit form, for the rest of the fields.' +package: Custom +core_version_requirement: ^9 || ^10 diff --git a/packages/drupal/entity_create_split/entity_create_split.module b/packages/drupal/entity_create_split/entity_create_split.module new file mode 100644 index 000000000..2ae2e596f --- /dev/null +++ b/packages/drupal/entity_create_split/entity_create_split.module @@ -0,0 +1,31 @@ +setFormClass('split', $entity_type->getFormClass('default')); + } + } +} + +/** + * Implements hook_gutenberg_enabled(). + */ +function entity_create_split_gutenberg_enabled(EntityInterface $entity) { + if (isset($entity->disableGutenberg) && $entity->disableGutenberg === TRUE) { + return FALSE; + } +} diff --git a/packages/drupal/entity_create_split/entity_create_split.routing.yml b/packages/drupal/entity_create_split/entity_create_split.routing.yml new file mode 100644 index 000000000..e5762db49 --- /dev/null +++ b/packages/drupal/entity_create_split/entity_create_split.routing.yml @@ -0,0 +1,9 @@ +entity_create_split.create: + path: '/entity/create/{entity_type}/{bundle}' + defaults: + _controller: '\Drupal\entity_create_split\Controller\EntityCreateSplitController::createForm' + _title_callback: '\Drupal\entity_create_split\Controller\EntityCreateSplitController::getTitle' + requirements: + _custom_access: '\Drupal\entity_create_split\Controller\EntityCreateSplitController::access' + options: + _admin_route: TRUE diff --git a/packages/drupal/entity_create_split/entity_create_split.services.yml b/packages/drupal/entity_create_split/entity_create_split.services.yml new file mode 100644 index 000000000..4216b1192 --- /dev/null +++ b/packages/drupal/entity_create_split/entity_create_split.services.yml @@ -0,0 +1,9 @@ +services: + entity_create_split.route_subscriber: + class: Drupal\entity_create_split\EventSubscriber\EntityCreateSplitRequestSubscriber + arguments: + - "@current_route_match" + - "@entity_type.manager" + - "@entity_display.repository" + tags: + - { name: event_subscriber } diff --git a/packages/drupal/entity_create_split/src/Controller/EntityCreateSplitController.php b/packages/drupal/entity_create_split/src/Controller/EntityCreateSplitController.php new file mode 100644 index 000000000..f27b6b2fa --- /dev/null +++ b/packages/drupal/entity_create_split/src/Controller/EntityCreateSplitController.php @@ -0,0 +1,61 @@ +entityTypeManager()->getDefinition($entity_type); + $bundleKey = $entityTypeDefinition->getKey('bundle'); + $entity = $this->entityTypeManager()->getStorage($entity_type)->create([$bundleKey => $bundle]); + $entity->disableGutenberg = TRUE; + $editForm = $this->entityTypeManager()->getFormObject($entity_type, 'split')->setEntity($entity); + return \Drupal::formBuilder()->getForm($editForm); + } + + /** + * Title callback for the createForm() route. + */ + public function getTitle($entity_type, $bundle) { + $entityTypeDefinition = $this->entityTypeManager()->getDefinition($entity_type); + $bundleEntityType = $entityTypeDefinition->getBundleEntityType(); + $bundleLabel = $this->entityTypeManager()->getStorage($bundleEntityType)->load($bundle)->label(); + return $this->t("Create %entity_type: %entity_bundle", [ + '%entity_type' => $entityTypeDefinition->getLabel(), + '%entity_bundle' => $bundleLabel, + ]); + } + + /** + * Access callback for the createForm() route. + */ + public function access($entity_type, $bundle) { + if (!$this->entityTypeManager()->hasDefinition($entity_type)) { + return AccessResult::forbidden(); + } + $entityTypeDefinition = $this->entityTypeManager()->getDefinition($entity_type); + $bundleEntityType = $entityTypeDefinition->getBundleEntityType(); + $bundleEntity = $this->entityTypeManager()->getStorage($bundleEntityType)->load($bundle); + if (!$bundleEntity) { + return AccessResult::forbidden(); + } + + return $this->entityTypeManager() + ->getAccessControlHandler($entity_type) + ->createAccess($bundle, NULL, [], TRUE); + } +} diff --git a/packages/drupal/entity_create_split/src/EventSubscriber/EntityCreateSplitRequestSubscriber.php b/packages/drupal/entity_create_split/src/EventSubscriber/EntityCreateSplitRequestSubscriber.php new file mode 100644 index 000000000..10f325d37 --- /dev/null +++ b/packages/drupal/entity_create_split/src/EventSubscriber/EntityCreateSplitRequestSubscriber.php @@ -0,0 +1,108 @@ +routeMatch = $route_match; + $this->entityTypeManager = $entity_type_manager; + $this->entityDisplayRepository = $entity_display_repository; + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = ['onKernelRequest', 0]; + return $events; + } + + public function onKernelRequest(RequestEvent $event) { + $supportedRoutes = $this->supportedRoutes(); + $currentRouteName = $this->routeMatch->getRouteName(); + if (empty($supportedRoutes[$currentRouteName])) { + return; + } + $entityTypeId = $supportedRoutes[$currentRouteName]; + $entityTypeDefinition =$this->entityTypeManager->getDefinition($entityTypeId); + // Make sure the entity has the split form class handler defined. + if (!$entityTypeDefinition->getFormClass('split')) { + return; + } + $bundleEntityType = $entityTypeDefinition->getBundleEntityType(); + $bundle = $this->routeMatch->getParameter($bundleEntityType); + // If, for some reason, we can't load the bundle from the current request, + // then we just stop here. + if (empty($bundle)) { + return; + } + $formModes = $this->entityDisplayRepository->getFormModeOptionsByBundle($entityTypeId, $bundle->id()); + // If the "split" form mode is not setup on the current entity bundle, then + // we also just stop here. + if (empty($formModes['split'])) { + return; + } + + // If we got here, it means that we are on a entity add route, and the + // entity bundle has the split form mode configure, which means we need to + // redirect to the entity_create_split.create route for that entity bundle. + $createSplitUrl = Url::fromRoute('entity_create_split.create', [ + 'entity_type' => $entityTypeId, + 'bundle' => $bundle->id(), + ]); + $response = new TrustedRedirectResponse($createSplitUrl->setAbsolute() + ->toString(), 302); + $event->setResponse($response); + } + + /** + * Helper, temporary, method to define the routes which are checked by the + * event subscriber for redirecting the user to the split form. + * + * @return string[] + */ + protected function supportedRoutes() { + return [ + 'node.add' => 'node', + 'media.add' => 'media', + ]; + } +} diff --git a/packages/drupal/test_content/webforms/styling.yml b/packages/drupal/test_content/webforms/styling.yml index 7cfdd2291..b73dbb9c2 100644 --- a/packages/drupal/test_content/webforms/styling.yml +++ b/packages/drupal/test_content/webforms/styling.yml @@ -25,11 +25,17 @@ elements: |- Foo: Foo Bar: 'Bar -- Bar has a description.' Baz: Baz - textfield: - '#type': textfield - '#title': Textfield - '#description': 'Textfield description.' - '#placeholder': 'Placeholder goes here' + flexbox: + '#type': webform_flexbox + textfield: + '#type': textfield + '#title': 'First name' + '#description': 'Textfield description.' + '#placeholder': 'Placeholder goes here' + '#required': true + last_name: + '#type': textfield + '#title': 'Last name' email_with_a_conformation: '#type': webform_email_confirm '#title': 'Email with a confirmation' @@ -56,6 +62,22 @@ elements: |- Foo: Foo Bar: Bar Baz: Baz + terms_of_service: + '#type': webform_terms_of_service + '#title': 'I agree to the {terms of service} in modal' + '#required': true + '#terms_content': |- + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a felis convallis, pharetra mauris a, eleifend justo. Sed massa ipsum, suscipit sit amet varius a, dignissim at ex. Integer euismod a sapien id auctor. Etiam dignissim scelerisque nunc in porttitor. Nam rutrum molestie mauris, a bibendum ligula. Etiam eget posuere enim. Quisque id condimentum ligula. + + Duis sit amet sapien justo. Suspendisse potenti. Aliquam venenatis congue quam, quis eleifend tellus. Duis aliquam sollicitudin purus sed rutrum. Nullam vitae ante lorem. Curabitur malesuada nibh lectus, ut ultricies elit mattis sed. Ut pharetra urna nec lacus pellentesque vestibulum. Nulla turpis sem, aliquam finibus pharetra at, interdum sit amet ligula. In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce ullamcorper justo in risus condimentum suscipit. Nam in nunc sit amet neque fringilla suscipit. Sed nec metus tempus, rhoncus dolor et, vehicula urna. Donec ultricies leo pharetra ante vulputate vulputate. Nullam tincidunt pharetra nisi eu faucibus. Duis efficitur, massa eget sagittis sodales, magna dolor dapibus justo, et facilisis tellus augue et est. + terms_of_service_01: + '#type': webform_terms_of_service + '#title': 'I agree to the {terms of service} in slideout' + '#terms_type': slideout + '#terms_content': |- + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a felis convallis, pharetra mauris a, eleifend justo. Sed massa ipsum, suscipit sit amet varius a, dignissim at ex. Integer euismod a sapien id auctor. Etiam dignissim scelerisque nunc in porttitor. Nam rutrum molestie mauris, a bibendum ligula. Etiam eget posuere enim. Quisque id condimentum ligula. + + Duis sit amet sapien justo. Suspendisse potenti. Aliquam venenatis congue quam, quis eleifend tellus. Duis aliquam sollicitudin purus sed rutrum. Nullam vitae ante lorem. Curabitur malesuada nibh lectus, ut ultricies elit mattis sed. Ut pharetra urna nec lacus pellentesque vestibulum. Nulla turpis sem, aliquam finibus pharetra at, interdum sit amet ligula. In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce ullamcorper justo in risus condimentum suscipit. Nam in nunc sit amet neque fringilla suscipit. Sed nec metus tempus, rhoncus dolor et, vehicula urna. Donec ultricies leo pharetra ante vulputate vulputate. Nullam tincidunt pharetra nisi eu faucibus. Duis efficitur, massa eget sagittis sodales, magna dolor dapibus justo, et facilisis tellus augue et est. actions: '#type': webform_actions '#title': 'Submit button(s)' diff --git a/packages/ui/src/components/Organisms/PageContent/BlockForm.stories.tsx b/packages/ui/src/components/Organisms/PageContent/BlockForm.stories.tsx index 4b9471c37..a0311eca8 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockForm.stories.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockForm.stories.tsx @@ -19,6 +19,26 @@ export const Error = { args: { url: 'webforms/error/index.html' as Url, cssStylesToInject: cmsCss, - className: 'mt-16', + }, +} satisfies StoryObj; + +export const TermsOfServiceModal = { + args: { + url: 'webforms/terms-of-service-modal/index.html' as Url, + cssStylesToInject: cmsCss, + }, + parameters: { + chromatic: { + // Makes no sense to test on different viewports because the modal size + // and position are hardcoded in the form snapshot. + viewports: [1440], + }, + }, +} satisfies StoryObj; + +export const TermsOfServiceSlideout = { + args: { + url: 'webforms/terms-of-service-slideout/index.html' as Url, + cssStylesToInject: cmsCss, }, } satisfies StoryObj; diff --git a/packages/ui/src/components/Organisms/PageHero.tsx b/packages/ui/src/components/Organisms/PageHero.tsx index 55a1088ca..61b9ea3ea 100644 --- a/packages/ui/src/components/Organisms/PageHero.tsx +++ b/packages/ui/src/components/Organisms/PageHero.tsx @@ -107,7 +107,7 @@ function FormHero(props: NonNullable) { {props.formUrl ? (
-
+
diff --git a/packages/ui/src/iframe.css b/packages/ui/src/iframe.css index f0599c01a..8633ffd05 100644 --- a/packages/ui/src/iframe.css +++ b/packages/ui/src/iframe.css @@ -7,6 +7,20 @@ h1 { @apply hidden; } +/* Skip to main content link on iframe */ +.visually-hidden.focusable { + @apply sr-only; +} + +.visually-hidden.focusable:active, .visually-hidden.focusable:focus { + @apply not-sr-only ml-1; +} + +/* Horizontal padding to avoid focus style to be cropped by the iframe */ +.webform-submission-form { + @apply px-1 text-gray-900; +} + /* Vertical spacing */ .webform-submission-form > *:not(:last-child) { @apply mb-10; @@ -31,7 +45,7 @@ h1 { /* field description */ .form-item .webform-element-description { - @apply text-sm text-gray-600; + @apply text-sm text-gray-500; } .form-item .description { @@ -63,7 +77,7 @@ h1 { /* checkbox */ .form-item input[type="checkbox"] { - @apply rounded; + @apply rounded -mt-1; } .form-item input[type="checkbox"]:not(:checked), .form-item input[type="radio"]:not(:checked) { @@ -83,10 +97,15 @@ h1 { @apply text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center; } -.form-item input.error { +.form-item input.error, .form-item textarea.error, .form-item select.error, .form-item input[type="checkbox"].error { @apply border-red-500; } +.form-required:after { + @apply content-['*'] text-gray-900 pl-1; +} + + .fieldset-legend { @apply font-bold mb-2 block; } @@ -94,3 +113,56 @@ h1 { [data-drupal-messages] [role="alert"] { @apply text-red-500; } + +/* terms of service field */ +.form-type-webform-terms-of-service { + @apply text-gray-500 text-sm; +} + +.form-type-webform-terms-of-service a { + @apply text-gray-900 underline; +} + +.form-type-webform-terms-of-service a:hover { + @apply text-gray-900 no-underline; +} + +/* terms of service field - Modal */ +.ui-widget-overlay { + @apply opacity-0 pointer-events-none; +} + +.ui-widget.ui-widget-content { + @apply bg-white border border-gray-300 rounded-lg px-0; +} + +.ui-widget.ui-widget-content .ui-widget-header { + @apply bg-white border-0 border-b border-gray-300 py-3 px-10; +} + +.ui-widget.ui-widget-content .ui-button { + @apply border-0; +} + +.ui-dialog .ui-dialog-titlebar-close { + @apply right-5; +} + +.ui-dialog .ui-button .ui-icon-closethick, .ui-dialog .ui-button:hover .ui-icon-closethick { + background-image: url("data:image/svg+xml;utf8,"); + @apply bg-no-repeat bg-center h-10 w-10 -left-0.5 -top-0.5 rounded-lg; +} + +.ui-dialog .ui-button:hover .ui-icon-closethick { + background-image: url("data:image/svg+xml;utf8,"); + @apply text-red-600 bg-gray-100; +} + +.ui-dialog .ui-button:focus { + @apply bg-white; +} + +/* terms of service field - Dropdown */ +.webform-terms-of-service-details { + @apply text-gray-900 border-gray-300 border rounded-lg p-5 leading-relaxed; +} diff --git a/tests/e2e/specs/drupal/content-editing.spec.ts b/tests/e2e/specs/drupal/content-editing.spec.ts index 463d49dc7..b90810480 100644 --- a/tests/e2e/specs/drupal/content-editing.spec.ts +++ b/tests/e2e/specs/drupal/content-editing.spec.ts @@ -6,19 +6,21 @@ test.describe('content-editing', () => { test.use({ storageState: '.auth/admin.json' }); test('moderation controls are placed in the sidebar', async ({ page }) => { - await page.goto(cmsUrl('/node/add/page')); + await page.goto(cmsUrl('/drupal')); + await page.locator('li.tabs__tab a:text("Edit")').click(); await expect( - page.locator('[aria-label="Editor settings"]').getByText('Save as'), + page.locator('[aria-label="Editor settings"]').getByText('Change to'), ).toBeVisible(); }); test('"More settings" fieldset is removed', async ({ page }) => { + await page.goto(cmsUrl('/drupal')); + await page.locator('li.tabs__tab a:text("Edit")').click(); // Why we expect it to be removed: // - It's too long to scroll to the bottom of long pages // - If we have any valuable controls in the "More settings" fieldset, // we should move them to the sidebar, where they are much easier to // access - await page.goto(cmsUrl('/node/add/page')); await expect(page.locator(':text-is("More settings")')).toHaveCount(0); }); }); diff --git a/tests/e2e/specs/drupal/preview.spec.ts b/tests/e2e/specs/drupal/preview.spec.ts index 78865a978..4f4f0d880 100644 --- a/tests/e2e/specs/drupal/preview.spec.ts +++ b/tests/e2e/specs/drupal/preview.spec.ts @@ -10,6 +10,8 @@ test.describe('instant preview', () => { await page .getByLabel('Title', { exact: true }) .fill('Instant preview test'); + await page.locator('#edit-submit').click(); + await page.locator('li.tabs__tab a:text("Edit")').click(); await page .locator('#editor-edit-body-0-value h1 span') .first() diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json index 87555134a..ed6fc0aa9 100644 --- a/tests/e2e/tsconfig.json +++ b/tests/e2e/tsconfig.json @@ -3,7 +3,7 @@ "strict": true, "skipLibCheck": true, "esModuleInterop": true, - "lib": ["es2015"], + "lib": ["es2015", "DOM"], "types": ["node"] } } diff --git a/tests/e2e/webform-snapshots/webforms.spec.ts b/tests/e2e/webform-snapshots/webforms.spec.ts index bdd7fbb51..1fe4ba862 100644 --- a/tests/e2e/webform-snapshots/webforms.spec.ts +++ b/tests/e2e/webform-snapshots/webforms.spec.ts @@ -5,21 +5,61 @@ import { execSync } from 'child_process'; import { cmsUrl } from '../helpers/url'; const baseDir = '../../packages/ui/static/stories/webforms'; +const iframeResizerSelector = + 'script[src*="silverback_iframe/js/iframeResizer.contentWindow.min.js"]'; test('Export webforms for styling', async ({ page }) => { execSync(`rm -rf ${baseDir}`); const baseUrl = cmsUrl('/en/form/styling?iframe=true&no_css=true'); + page.setViewportSize({ + // In the frontend the form is wrapped into a div with max-w-3xl class, + // which translates to 48 rem, which is 768 px. Roughly. + // We try to match the width because the modal is centered dynamically by + // Drupal JS, but then, when we save the page, the size and position are + // fixed in the HTML. + width: 768, + // Does not matter. + height: 768, + }); + await page.goto(baseUrl); + await expect(page.locator(iframeResizerSelector)).toHaveCount(1); await savePage(page, 'idle'); + // Disable frontend validation. + await page.evaluate(() => { + document.querySelectorAll('[required]').forEach((element) => { + element.removeAttribute('required'); + }); + }); + await page.getByLabel('Email with a confirmation').fill('asd@asd'); await page.getByRole('button', { name: 'Submit' }).click(); await expect( page.locator('[data-drupal-messages] [role="alert"]'), ).toBeVisible(); + await expect(page.locator(iframeResizerSelector)).toHaveCount(1); await savePage(page, 'error'); + + await page.goto(baseUrl); + await page + .locator('label') + .filter({ hasText: 'I agree to the terms of service in modal' }) + .getByRole('button') + .click(); + await page.waitForTimeout(500); + await savePage(page, 'terms-of-service-modal'); + + await page.goto(baseUrl); + await page + .locator('label') + .filter({ hasText: 'I agree to the terms of service in slideout' }) + .getByRole('button') + .click(); + await page.waitForTimeout(500); + await savePage(page, 'terms-of-service-slideout'); }); async function savePage(page: Page, name: string) {