Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
446afab
Add real preview/publish e2e tests and shared createDocument helper
mhaack Jul 14, 2026
d61af03
Match copy_rename.spec.js pattern for document creation/save wait
mhaack Jul 14, 2026
33259fc
Open previewed/published page in a new tab and verify content
mhaack Jul 14, 2026
6ce7df4
Add bulk preview/publish e2e tests for 12 pages in a folder
mhaack Jul 14, 2026
57fb2a4
Scope select-all locator to browse list to avoid strict-mode dup id v…
mhaack Jul 14, 2026
5f1ec35
fix: remove test comment
mhaack Jul 14, 2026
b961227
Merge branch 'main' into prevpub
mhaack Jul 14, 2026
0b4f005
Add delete + unpublish cleanup to all 4 preview/publish e2e tests
mhaack Jul 15, 2026
5817a9b
Delete the empty test folder after bulk preview/publish cleanup
mhaack Jul 15, 2026
089cb48
Skip unpublish checkbox in cleanup for now (plain delete only)
mhaack Jul 15, 2026
48774fc
Simplify cleanup delete: drop typed-YES confirmation for bulk deletes…
mhaack Jul 15, 2026
0643d3c
Remove explicit cleanup; rely on scheduled delete.spec.js sweep instead
mhaack Jul 15, 2026
c8288ca
Dismiss optional alert banner on first browse-view load in each test
mhaack Jul 15, 2026
4bda986
Target the alert banner's Dismiss button by accessible name
mhaack Jul 15, 2026
8dc8b6e
fix: improve test stability
mhaack Jul 15, 2026
e1ccbd7
Make saveToAem HLX6-aware via nx aem.preview/aem.publish API
mhaack Jul 15, 2026
9a3b30e
fix: stabilize test
mhaack Jul 15, 2026
18268d2
Share one set of 12 bulk test pages between preview and publish tests
mhaack Jul 15, 2026
4e8b2c0
Raise beforeAll hook timeout for bulk page creation
mhaack Jul 15, 2026
668814c
fix: test
mhaack Jul 16, 2026
03f2b31
fix: stabilizing
mhaack Jul 16, 2026
61e33f7
Move dismissAlertBanner to shared test/e2e/utils/utils.js
mhaack Jul 16, 2026
e743ff8
Dismiss alert banner in delete.spec.js's old-pages cleanup test
mhaack Jul 16, 2026
4b72e32
Dismiss alert banner in the empty-open-editors delete test too
mhaack Jul 16, 2026
acfd4ae
Dismiss alert banner in edit.spec.js's Create Delete Document test
mhaack Jul 16, 2026
3104620
Dismiss alert banner in copy_rename.spec.js
mhaack Jul 16, 2026
bff7d4e
Only wait for alert banner when it isn't already present
mhaack Jul 16, 2026
9203f43
fix: restore test config
mhaack Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions blocks/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion test/e2e/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,3 @@ module.exports = defineConfig({
// reuseExistingServer: !process.env.CI,
// },
});

6 changes: 5 additions & 1 deletion test/e2e/tests/copy_rename.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
`,
);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/tests/delete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down
16 changes: 11 additions & 5 deletions test/e2e/tests/edit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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');
Expand Down
194 changes: 189 additions & 5 deletions test/e2e/tests/preview_publish.spec.js
Original file line number Diff line number Diff line change
@@ -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 })
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');

Expand Down
Loading
Loading