diff --git a/blocks/shared/utils.js b/blocks/shared/utils.js index 3e30b52ba..1fc4d36fa 100644 --- a/blocks/shared/utils.js +++ b/blocks/shared/utils.js @@ -168,11 +168,14 @@ export function parseAemError(xError) { return xError.replace('[admin] ', ''); } +// `action` is the admin API namespace: 'preview' or 'live' (the latter is the +// admin API's name for publish). Routes through nx's `aem` API, which detects +// HLX6 vs legacy (via isHlx6) and calls the correct endpoint for the org/site. export async function saveToAem(path, action) { - const [owner, repo, ...parts] = path.slice(1).toLowerCase().split('/'); - const aemPath = parts.join('/'); - const url = `${AEM_ORIGIN}/${action}/${owner}/${repo}/main/${aemPath}`; - const resp = await daFetch(url, { method: 'POST' }); + const { aem } = await getNx2Api(); + const aemPath = path.toLowerCase(); + const call = action === 'live' ? aem.publish : aem.preview; + const resp = await call(aemPath); if (!resp.ok) { const { status, headers } = resp; const authErr = [401, 403].some((s) => s === status); diff --git a/test/e2e/playwright.config.js b/test/e2e/playwright.config.js index 5addc1f82..61ceae8c4 100644 --- a/test/e2e/playwright.config.js +++ b/test/e2e/playwright.config.js @@ -95,4 +95,3 @@ module.exports = defineConfig({ // reuseExistingServer: !process.env.CI, // }, }); - diff --git a/test/e2e/tests/copy_rename.spec.js b/test/e2e/tests/copy_rename.spec.js index d16fb55f5..bd1d0a6b1 100644 --- a/test/e2e/tests/copy_rename.spec.js +++ b/test/e2e/tests/copy_rename.spec.js @@ -14,12 +14,13 @@ import ENV from '../utils/env.js'; import { getQuery, getTestFolderURL, getTestPageURL, fill, TEST_ORG, TEST_SITE, } from '../utils/page.js'; +import { dismissAlertBanner } from '../utils/utils.js'; test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => { test.skip( TEST_SITE !== 'da-status', ` -On Helix 6 the copy and paste from one folder to another doesn't work yet, it fails on this line: +On Helix 6 the copy and paste from one folder to another doesn't work yet, it fails on this line: const link = await page.getByRole('link', { name: orgPageName }); `, ); @@ -61,6 +62,7 @@ const link = await page.getByRole('link', { name: orgPageName }); // Go back to the directory view await page.goto(`${ENV}/${getQuery()}#/${TEST_ORG}/${TEST_SITE}/tests`); + await dismissAlertBanner(page); const copyFolderURL = getTestFolderURL('copy', workerInfo); const copyFolderName = copyFolderURL.split('/').pop(); @@ -91,6 +93,8 @@ const link = await page.getByRole('link', { name: orgPageName }); await page.goto(`${ENV}/${getQuery()}#/${TEST_ORG}/${TEST_SITE}/tests`); await page.reload(); // Clears any leftover selection, if any + await dismissAlertBanner(page); + const checkbox = page.locator('div.da-item-list-item-inner').filter({ hasText: orgPageName }) .locator('input[type="checkbox"][name="item-selected"]'); await checkbox.focus(); diff --git a/test/e2e/tests/delete.spec.js b/test/e2e/tests/delete.spec.js index 8634ce60c..e80be835b 100644 --- a/test/e2e/tests/delete.spec.js +++ b/test/e2e/tests/delete.spec.js @@ -14,6 +14,7 @@ import ENV from '../utils/env.js'; import { getQuery, getTestPageURL, getTestResourceAge, tabBackward, fill, TEST_ORG, TEST_SITE, } from '../utils/page.js'; +import { dismissAlertBanner } from '../utils/utils.js'; // Files are deleted after 2 hours by default const MIN_HOURS = process.env.PW_DELETE_HOURS ? Number(process.env.PW_DELETE_HOURS) : 2; @@ -36,6 +37,8 @@ test('Delete multiple old pages', async ({ page }, workerInfo) => { // This page will always be there as its used by a test await expect(page.getByText('pingtest'), 'Precondition').toBeVisible(); + await dismissAlertBanner(page); + // List the resources and check fot the ones that are to be deleted. These are always pages // created by the getTestPageURL() function in page.js const items = page.locator('.da-item-list-item-name'); @@ -87,6 +90,8 @@ test('Delete multiple old pages', async ({ page }, workerInfo) => { // Wait for the delete button to disappear which is when we're done await expect(page.locator('button.delete-button').filter({ visible: true })).not.toBeVisible({ timeout: 600000 }); + + console.log('Deleted', itemsToDelete, 'test files and folders'); }); test('Empty out open editors on deleted documents', async ({ browser, page }, workerInfo) => { @@ -126,6 +131,7 @@ test('Empty out open editors on deleted documents', async ({ browser, page }, wo await tabBackward(list); await list.keyboard.press(' '); await list.waitForTimeout(500); + await dismissAlertBanner(list); await list.locator('button.delete-button').filter({ visible: true }).click(); // Give the modal a chance to open diff --git a/test/e2e/tests/edit.spec.js b/test/e2e/tests/edit.spec.js index 4f30e2850..cfefbc769 100644 --- a/test/e2e/tests/edit.spec.js +++ b/test/e2e/tests/edit.spec.js @@ -14,12 +14,14 @@ import ENV from '../utils/env.js'; import { getQuery, getTestPageURL, tabBackward, fill, TEST_ORG, TEST_SITE, } from '../utils/page.js'; +import { dismissAlertBanner } from '../utils/utils.js'; test('Update Document', async ({ browser, page }, workerInfo) => { test.setTimeout(30000); const url = getTestPageURL('edit1', workerInfo); await page.goto(url); + await page.waitForTimeout(2000); await page.getByText('Create document', { exact: true }).click(); await expect(page.locator('div.ProseMirror')).toBeVisible(); await expect(page.locator('div.ProseMirror')).toHaveAttribute('contenteditable', 'true'); @@ -59,6 +61,7 @@ test('Create Delete Document', async ({ browser, page }, workerInfo) => { const newPage = await browser.newPage(); await newPage.goto(`${ENV}/${getQuery()}#/${TEST_ORG}/${TEST_SITE}/tests`); + await dismissAlertBanner(newPage); await newPage.waitForTimeout(3000); await newPage.reload(); @@ -70,6 +73,7 @@ test('Create Delete Document', async ({ browser, page }, workerInfo) => { await newPage.waitForTimeout(500); await page.close(); // Close the original page to avoid it writing the content + await dismissAlertBanner(newPage); // There are 2 delete buttons, one on the Browse panel and another on the Search one // select the visible one. await newPage.locator('button.delete-button').filter({ visible: true }).click(); @@ -116,8 +120,10 @@ test('Change document by switching anchors', async ({ page }, workerInfo) => { await expect(page.locator('div.ProseMirror')).toBeVisible(); await expect(page.locator('div.ProseMirror')).toHaveAttribute('contenteditable', 'true'); // Allow Y.js WebSocket to stabilize before typing - await page.waitForTimeout(3000); + await page.waitForTimeout(2000); await fill(page, 'page B'); + await page.waitForTimeout(3000); + // Verify the fill took effect locally before waiting for persistence await expect(page.locator('div.ProseMirror')).toContainText('page B'); // Wait for Y.js to persist the content to the server @@ -154,12 +160,12 @@ test('Add code mark', async ({ page }, workerInfo) => { // Forward for (let i = 0; i < 10; i += 1) { await page.keyboard.press('ArrowLeft'); - await page.waitForTimeout(100); + await page.waitForTimeout(200); } await page.keyboard.press('`'); for (let i = 0; i < 4; i += 1) { await page.keyboard.press('ArrowRight'); - await page.waitForTimeout(100); + await page.waitForTimeout(200); } await page.keyboard.press('`'); // leave time for the code mark to be processed @@ -174,13 +180,13 @@ test('Add code mark', async ({ page }, workerInfo) => { await page.keyboard.press('End'); for (let i = 0; i < 6; i += 1) { await page.keyboard.press('ArrowLeft'); - await page.waitForTimeout(100); + await page.waitForTimeout(200); } await page.keyboard.press('`'); await page.locator('div.ProseMirror').locator('code'); for (let i = 0; i < 5; i += 1) { await page.keyboard.press('ArrowLeft'); - await page.waitForTimeout(100); + await page.waitForTimeout(200); } await page.keyboard.press('`'); codeElement = proseMirror.locator('code'); diff --git a/test/e2e/tests/preview_publish.spec.js b/test/e2e/tests/preview_publish.spec.js index c20cd3323..d52cfdba4 100644 --- a/test/e2e/tests/preview_publish.spec.js +++ b/test/e2e/tests/preview_publish.spec.js @@ -1,10 +1,15 @@ import { test, expect } from '@playwright/test'; import ENV from '../utils/env.js'; -import { getQuery, TEST_ORG, TEST_SITE } from '../utils/page.js'; +import { + getQuery, getTestPageURL, getTestFolderURL, createDocument, fill, TEST_ORG, TEST_SITE, +} from '../utils/page.js'; +import { dismissAlertBanner } from '../utils/utils.js'; // Requires write access to TEST_SITE. pingtest must exist in the /tests directory. const TESTS_DIR = `${ENV}/${getQuery()}#/${TEST_ORG}/${TEST_SITE}/tests`; +const BULK_PAGE_COUNT = 12; + async function selectItem(page, name) { const checkbox = page .locator('div.da-item-list-item-inner').filter({ hasText: name, exact: true }) @@ -13,9 +18,53 @@ async function selectItem(page, name) { await page.keyboard.press(' '); } +// Creates a folder under the tests directory and returns both its URL and the +// hash-path portion (e.g. /org/site/tests/pw-foo-123-chromium) so pages can be +// created directly inside it via getTestPageURL(..., folderPath). +async function createFolder(page, workerInfo, testIdentifier) { + const folderURL = getTestFolderURL(testIdentifier, workerInfo); + const folderName = folderURL.split('/').pop(); + + await page.goto(TESTS_DIR); + await dismissAlertBanner(page); + await expect(page.getByRole('button', { name: 'New' })).toBeEnabled(); + await page.getByRole('button', { name: 'New' }).click({ force: true }); + await page.getByRole('menuitem', { name: 'Folder' }).click(); + await page.getByPlaceholder('folder name').fill(folderName); + await page.getByRole('button', { name: 'Create' }).click(); + + const folderPath = new URL(folderURL).hash.slice(1); + return { folderURL, folderPath }; +} + +// Creates `count` pages inside the given folder, each with some distinguishing +// text content, and returns the list of page names created. +async function createPagesInFolder(page, workerInfo, folderPath, prefix, count) { + const pageNames = []; + for (let i = 0; i < count; i += 1) { + const url = getTestPageURL(`${prefix}${i}`, workerInfo, folderPath); + const pageName = url.split('/').pop(); + pageNames.push(pageName); + + // eslint-disable-next-line no-await-in-loop + await createDocument(page, url); + + // Allow Y.js WebSocket to stabilize before typing + // eslint-disable-next-line no-await-in-loop + await page.waitForTimeout(2000); + + // eslint-disable-next-line no-await-in-loop + await fill(page, `${prefix} test ${i}`); + + // eslint-disable-next-line no-await-in-loop + await page.waitForTimeout(3000); + } + return pageNames; +} + test('Preview and Publish buttons appear when a file is selected', async ({ page }) => { - test.skip(TEST_SITE !== 'da-status', 'Requires write access to da-status'); await page.goto(TESTS_DIR); + await dismissAlertBanner(page); await expect(page.getByText('pingtest'), 'Precondition: pingtest must exist').toBeVisible(); await selectItem(page, 'pingtest'); @@ -25,8 +74,8 @@ test('Preview and Publish buttons appear when a file is selected', async ({ page }); test('Clicking Preview opens a confirmation dialog', async ({ page }) => { - test.skip(TEST_SITE !== 'da-status', 'Requires write access to da-status'); await page.goto(TESTS_DIR); + await dismissAlertBanner(page); await expect(page.getByText('pingtest'), 'Precondition: pingtest must exist').toBeVisible(); await selectItem(page, 'pingtest'); @@ -36,10 +85,145 @@ test('Clicking Preview opens a confirmation dialog', async ({ page }) => { await expect(page.locator('da-dialog')).toContainText('Preview the'); }); +test('Preview the selected page', async ({ page, context }, workerInfo) => { + test.setTimeout(60000); + + const url = getTestPageURL('preview', workerInfo); + const pageName = url.split('/').pop(); + await createDocument(page, url); + + // Allow Y.js WebSocket to stabilize before typing + await page.waitForTimeout(2000); + await fill(page, 'preview test'); + + // Wait to ensure its saved in da-admin + await page.waitForTimeout(3000); + + await page.goto(TESTS_DIR); + await expect(page.getByText(pageName), 'Precondition: new page must exist').toBeVisible(); + + await dismissAlertBanner(page); + await selectItem(page, pageName); + await page.locator('button.preview-button').filter({ visible: true }).click(); + + await expect(page.locator('da-dialog')).toContainText('Preview the'); + await page.locator('sl-button.accent').filter({ visible: true }).click(); + + await expect(page.locator('button.da-aem-results-btn')).toBeVisible({ timeout: 30000 }); + await expect(page.locator('button.da-aem-results-btn')).toContainText('Previewed 1 item'); + await expect(page.locator('da-dialog').filter({ hasText: 'Errors' })).toHaveCount(0); + + // Open the previewed page in its own browser tab and confirm it actually rendered + await page.locator('button.da-aem-results-btn').click(); + const previewLink = page.locator('da-dialog a').first(); + await expect(previewLink).toBeVisible(); + const previewUrl = await previewLink.getAttribute('href'); + + const previewTab = await context.newPage(); + await previewTab.goto(previewUrl); + await expect(previewTab.locator('body')).toContainText('preview test'); + await previewTab.close(); +}); + +test('Publish the selected page', async ({ page, context }, workerInfo) => { + test.setTimeout(60000); + + const url = getTestPageURL('publish', workerInfo); + const pageName = url.split('/').pop(); + await createDocument(page, url); + + // Allow Y.js WebSocket to stabilize before typing + await page.waitForTimeout(2000); + await fill(page, 'publish test'); + + // Wait to ensure its saved in da-admin + await page.waitForTimeout(3000); + + await page.goto(TESTS_DIR); + await expect(page.getByText(pageName), 'Precondition: new page must exist').toBeVisible(); + await dismissAlertBanner(page); + + await selectItem(page, pageName); + await page.locator('button.publish-button').filter({ visible: true }).click(); + + await expect(page.locator('da-dialog')).toContainText('Publish the'); + await page.locator('sl-button.accent').filter({ visible: true }).click(); + + await expect(page.locator('button.da-aem-results-btn')).toBeVisible({ timeout: 30000 }); + await expect(page.locator('button.da-aem-results-btn')).toContainText('Published 1 item'); + await expect(page.locator('da-dialog').filter({ hasText: 'Errors' })).toHaveCount(0); + + // Open the published page in its own browser tab and confirm it actually rendered + await page.locator('button.da-aem-results-btn').click(); + const publishLink = page.locator('da-dialog a').first(); + await expect(publishLink).toBeVisible(); + const publishUrl = await publishLink.getAttribute('href'); + + const publishTab = await context.newPage(); + await publishTab.goto(publishUrl); + await expect(publishTab.locator('body')).toContainText('publish test'); + await publishTab.close(); +}); + +test.describe.serial('Bulk preview/publish 12 pages in a folder', () => { + // Created once in beforeAll and reused by both tests below, since creating + // 12 real pages is expensive and the same set can be previewed and then + // published in sequence. + let folderURL; + let folderPath; + + test.beforeAll(async ({ browser }, workerInfo) => { + // Creating 12 real pages takes well over the default 30s hook timeout. + test.setTimeout(180000); + + const page = await browser.newPage(); + ({ folderURL, folderPath } = await createFolder(page, workerInfo, 'bulk')); + await createPagesInFolder(page, workerInfo, folderPath, 'bulk', BULK_PAGE_COUNT); + await page.close(); + }); + + test('Preview 12 pages in a folder', async ({ page }) => { + test.setTimeout(120000); + + await page.goto(folderURL); + await expect(page.locator('div.da-item-list-item-inner')).toHaveCount(BULK_PAGE_COUNT); + await dismissAlertBanner(page); + + await page.locator('da-list.da-list-type-browse input#select-all').click(); + await page.locator('button.preview-button').filter({ visible: true }).click(); + + await expect(page.locator('da-dialog')).toContainText('Preview the'); + await page.locator('sl-button.accent').filter({ visible: true }).click(); + + await expect(page.locator('button.da-aem-results-btn')).toBeVisible({ timeout: 60000 }); + await expect(page.locator('button.da-aem-results-btn')).toContainText(`Previewed ${BULK_PAGE_COUNT} items`); + await expect(page.locator('da-dialog').filter({ hasText: 'Errors' })).toHaveCount(0); + }); + + test('Publish 12 pages in a folder', async ({ page }) => { + test.setTimeout(120000); + + await page.goto(folderURL); + await expect(page.locator('div.da-item-list-item-inner')).toHaveCount(BULK_PAGE_COUNT); + await dismissAlertBanner(page); + + await page.locator('da-list.da-list-type-browse input#select-all').click(); + await page.locator('button.publish-button').filter({ visible: true }).click(); + + await expect(page.locator('da-dialog')).toContainText('Publish the'); + await page.locator('sl-button.accent').filter({ visible: true }).click(); + + await expect(page.locator('button.da-aem-results-btn')).toBeVisible({ timeout: 60000 }); + await expect(page.locator('button.da-aem-results-btn')).toContainText(`Published ${BULK_PAGE_COUNT} items`); + await expect(page.locator('da-dialog').filter({ hasText: 'Errors' })).toHaveCount(0); + }); +}); + test('Preview and Publish buttons are hidden when only a folder is selected', async ({ page }) => { - test.skip(TEST_SITE !== 'da-status', 'Requires write access to da-status'); await page.goto(`${ENV}/${getQuery()}#/${TEST_ORG}/${TEST_SITE}`); - await expect(page.getByText('tests'), 'Precondition: tests folder must exist').toBeVisible(); + await dismissAlertBanner(page); + + await expect(page.getByRole('link', { name: 'tests' }), 'Precondition: tests folder must exist').toBeVisible(); await selectItem(page, 'tests'); diff --git a/test/e2e/utils/page.js b/test/e2e/utils/page.js index f34d41122..02093263e 100644 --- a/test/e2e/utils/page.js +++ b/test/e2e/utils/page.js @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +import { expect } from '@playwright/test'; import ENV, { TEST_ORG, TEST_SITE } from './env.js'; export { TEST_ORG, TEST_SITE }; @@ -81,6 +82,23 @@ export function getTestResourceAge(fileName) { const SELECT_ALL = process.platform === 'darwin' ? 'Meta+a' : 'Control+a'; +/** + * Navigates to a document URL and creates a new document from the blank-page prompt, + * waiting for the ProseMirror editor to become editable. Used by tests that need a + * fresh document to work with (editing, deleting, previewing, publishing, etc). + * + * @param {import('@playwright/test').Page} page + * @param {string} url - The URL of the (not yet existing) document to create. + */ +export async function createDocument(page, url) { + await page.goto(url); + await page.getByText('Create document', { exact: true }).click(); + await expect(page.locator('div.ProseMirror')).toBeVisible(); + await expect(page.locator('div.ProseMirror')).toHaveAttribute('contenteditable', 'true'); + // Allow Y.js WebSocket to stabilize before typing + await page.waitForTimeout(2000); +} + export async function fill(page, text) { const proseMirror = page.locator('div.ProseMirror'); await proseMirror.click(); diff --git a/test/e2e/utils/utils.js b/test/e2e/utils/utils.js new file mode 100644 index 000000000..42a563deb --- /dev/null +++ b/test/e2e/utils/utils.js @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * Some environments show a dismissible alert banner the first time the browse + * view loads. If present, dismiss it so it doesn't block later interactions. + * + * @param {import('@playwright/test').Page} page + */ +export async function dismissAlertBanner(page) { + const alert = page.getByRole('alert'); + + let visible = await alert.isVisible().catch(() => false); + if (!visible) { + // The banner can take a moment to appear; wait once and check again + // before giving up, but skip the wait entirely if it's already there. + await page.waitForTimeout(3000); + visible = await alert.isVisible().catch(() => false); + } + + if (visible) { + await alert.getByRole('button', { name: 'Dismiss' }).click(); + } +}