diff --git a/Taskfile.yml b/Taskfile.yml index 1ab7c192..e55fe823 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -327,12 +327,25 @@ tasks: test:e2e: dir: test/e2e - desc: Test tool using end-to-end tests using Playwright + desc: Test tool using end-to-end tests cmd: | set -e sh nw npm install sh nw npx playwright {{.CLI_ARGS | default "test"}} + test:e2e:codegen: + dir: test/e2e + desc: Generate end-to-end test code from recording + cmd: | + sh nw npm install + sh nw npx playwright codegen --target=ts {{.AEM_AUTHOR_HTTP_URL}} + + test:e2e:report: + dir: test/e2e + desc: Show end-to-end test report + cmd: | + sh nw npx playwright show-report + rde:setup: desc: setup access to RDE cmds: diff --git a/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java b/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java index 50b5f5e3..4625df4e 100644 --- a/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java +++ b/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java @@ -177,6 +177,7 @@ public void onEvent(Event event) { public void bootOnDemand() { LOG.info("Automatic scripts booting on demand - job scheduling"); + bootedScripts.clear(); unscheduleBoot(); scheduleBoot(); LOG.info("Automatic scripts booting on demand - job scheduled"); @@ -319,8 +320,10 @@ private void queueBootScript(Script script, ResourceResolver resourceResolver) { bootedScripts.put(script.getId(), checksum); LOG.info("Boot script '{}' queued", script.getId()); } else { - LOG.info("Boot script '{}' not eligible for queueing!", script.getId()); + LOG.info("Boot script '{}' not queued - check failed", script.getId()); } + } else { + LOG.info("Boot script '{}' not queued - checksum unchanged", script.getId()); } } diff --git a/test/e2e/001-general.spec.ts b/test/e2e/001-general.spec.ts index f178e422..acf6fbb0 100644 --- a/test/e2e/001-general.spec.ts +++ b/test/e2e/001-general.spec.ts @@ -1,24 +1,44 @@ import { test, expect } from '@playwright/test'; import { expectCodeExecutorStatus, expectHealthyStatus } from './utils/expect'; +import { testOnEnv, apiHeaders } from './utils/env'; +import { attachScreenshot } from './utils/page'; test.describe('General', () => { - test('Tool is accessible', async ({ page }) => { + test('Tool is accessible', async ({ page }, testInfo) => { await page.goto('/acm'); const title = page.locator('.granite-title', { hasText: 'Content Manager' }); await expect(title).toBeVisible(); + + await attachScreenshot(page, testInfo, 'Dashboard Page'); + }); + + testOnEnv('local')('Tool state is reset', async ({ page }) => { + const clearResponse = await page.request.post('/apps/acm/api/event.json?name=HISTORY_CLEAR', { headers: await apiHeaders(page) }); + expect(clearResponse.ok()).toBeTruthy(); + const clearJson = await clearResponse.json(); + expect(clearJson).toEqual({ status: 200, message: "Event 'HISTORY_CLEAR' dispatched successfully!", data: null }); + await page.waitForTimeout(3000); + + const bootResponse = await page.request.post('/apps/acm/api/event.json?name=SCRIPT_SCHEDULER_BOOT', { headers: await apiHeaders(page) }); + expect(bootResponse.ok()).toBeTruthy(); + const bootJson = await bootResponse.json(); + expect(bootJson).toEqual({ status: 200, message: "Event 'SCRIPT_SCHEDULER_BOOT' dispatched successfully!", data: null }); + await page.waitForTimeout(10000); }); - test('System is healthy', async ({ page }) => { + test('System is healthy', async ({ page }, testInfo) => { await page.goto('/acm#/maintenance?tab=health-checker'); await expectHealthyStatus(page); + await attachScreenshot(page, testInfo, 'Health Checker'); }); - test('Code executor is idle', async ({ page }) => { + test('Code executor is idle', async ({ page }, testInfo) => { await page.goto('/acm#/maintenance?tab=code-executor'); await expectCodeExecutorStatus(page); + await attachScreenshot(page, testInfo, 'Code Executor'); }); }); \ No newline at end of file diff --git a/test/e2e/002-console.spec.ts b/test/e2e/002-console.spec.ts index 1f84f746..e38831b3 100644 --- a/test/e2e/002-console.spec.ts +++ b/test/e2e/002-console.spec.ts @@ -1,9 +1,10 @@ import { test, expect } from '@playwright/test'; import { expectCompilationSucceeded, expectExecutionProgressBarSucceeded, expectToHaveMultilineText } from './utils/expect' import { readFromCodeEditor, writeToCodeEditor } from './utils/editor'; +import { attachScreenshot } from './utils/page'; test.describe('Console', () => { - test('Executes script', async ({ page }) => { + test('Executes script', async ({ page }, testInfo) => { await page.goto('/acm#/console'); await expectCompilationSucceeded(page); @@ -22,13 +23,14 @@ test.describe('Console', () => { await expect(page.getByRole('button', { name: 'Execute' })).toBeEnabled(); await page.getByRole('button', { name: 'Execute' }).click(); - await page.getByRole('tab', { name: 'Output' }).click(); + await expect(page.getByRole('tab', { name: 'Output' })).toHaveAttribute('aria-selected', 'true'); await expectExecutionProgressBarSucceeded(page); const output = await readFromCodeEditor(page, 'Console Output'); expectToHaveMultilineText(output, ` Hello World! `); + await attachScreenshot(page, testInfo, 'Console Output'); }); }); diff --git a/test/e2e/003-tool-access.spec.ts b/test/e2e/003-tool-access.spec.ts index 476d316f..c22dcc2f 100644 --- a/test/e2e/003-tool-access.spec.ts +++ b/test/e2e/003-tool-access.spec.ts @@ -2,9 +2,10 @@ import { test, expect } from '@playwright/test'; import { expectCompilationSucceeded, expectExecutionProgressBarSucceeded } from './utils/expect'; import { readFromCodeEditor, writeToCodeEditor } from './utils/editor'; import { newAemContext } from './utils/context'; +import { attachScreenshot } from './utils/page'; test.describe('Tool Access', () => { - test('Admin user has full access', async ({ page }) => { + test('Admin user has full access', async ({ page }, testInfo) => { await page.goto('/acm'); await expect(page.getByRole('button', { name: 'Console' })).toBeVisible(); @@ -18,9 +19,11 @@ test.describe('Tool Access', () => { await page.goto('/acm#/console'); await expectCompilationSucceeded(page); await expect(page.getByRole('button', { name: 'Execute' })).toBeEnabled(); + + await attachScreenshot(page, testInfo, 'Admin Full Access'); }); - test('Setup test user and verify limited access', async ({ page, browser }) => { + test('Setup test user and verify limited access', async ({ page, browser }, testInfo) => { await page.goto('/acm#/console'); await expectCompilationSucceeded(page); @@ -59,8 +62,7 @@ test.describe('Tool Access', () => { await expect(page.getByRole('button', { name: 'Execute' })).toBeEnabled(); await page.getByRole('button', { name: 'Execute' }).click(); - - await page.getByRole('tab', { name: 'Output' }).click(); + await expect(page.getByRole('tab', { name: 'Output' })).toHaveAttribute('aria-selected', 'true'); await expectExecutionProgressBarSucceeded(page); const output = await readFromCodeEditor(page, 'Console Output'); @@ -77,6 +79,8 @@ test.describe('Tool Access', () => { await expect(testUserPage.getByRole('button', { name: 'Snippets' })).not.toBeVisible(); await expect(testUserPage.getByRole('button', { name: 'History' })).not.toBeVisible(); await expect(testUserPage.getByRole('button', { name: 'Maintenance' })).not.toBeVisible(); + + await attachScreenshot(testUserPage, testInfo, 'Test User Access - Dashboard'); await testUserPage.getByRole('button', { name: 'Scripts' }).click(); await expect(testUserPage).toHaveURL(/\/acm#\/scripts/); @@ -90,6 +94,9 @@ test.describe('Tool Access', () => { const scriptRow = rows.nth(1); await expect(scriptRow.locator('[role="rowheader"]')).toContainText('example/ACME-200_hello-world'); + await attachScreenshot(testUserPage, testInfo, 'Test User Access - Script List'); + + // Check if routing blocks access to other tools await testUserPage.goto('/acm#/console'); await expect(testUserPage.getByRole('button', { name: 'Console' })).not.toBeVisible(); diff --git a/test/e2e/004-automatic-scripts.spec.ts b/test/e2e/004-automatic-scripts.spec.ts new file mode 100644 index 00000000..4baa5436 --- /dev/null +++ b/test/e2e/004-automatic-scripts.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; +import { attachScreenshot } from './utils/page'; + +test.describe('Automatic Scripts', () => { + + test('Executions saved in history', async ({ page }, testInfo) => { + await page.goto('/acm'); + await page.getByRole('button', { name: 'History' }).click(); + + const grid = page.locator('[role="grid"][aria-label="Executions table"]'); + await expect(grid).toBeVisible(); + const rows = grid.locator('[role="row"]'); + + await page.getByRole('searchbox', { name: 'Executable' }).fill('example/ACME-20_once'); + await expect(rows.nth(1)).toContainText('Script \'example/ACME-20_once\''); + await expect(rows.nth(1)).toContainText('succeeded'); + await attachScreenshot(page, testInfo, `Script List filtered by 'ACME-20_once'`); + + await page.getByRole('searchbox', { name: 'Executable' }).fill('example/ACME-21_changed'); + await expect(rows.nth(1)).toContainText('Script \'example/ACME-21_changed\''); + await expect(rows.nth(1)).toContainText('succeeded'); + await attachScreenshot(page, testInfo, `Script List filtered by 'ACME-21_changed'`); + }); +}); diff --git a/test/e2e/004-history.spec.ts b/test/e2e/004-history.spec.ts deleted file mode 100644 index 4936db50..00000000 --- a/test/e2e/004-history.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { readFromCodeEditor } from './utils/editor'; - -test.describe('History', () => { - test('Shows executions from console and tool access tests', async ({ page }) => { - await page.goto('/acm#/history'); - - await page.waitForTimeout(3000); - - const grid = page.locator('[role="grid"][aria-label="Executions table"]'); - await expect(grid).toBeVisible(); - - const rows = grid.locator('[role="row"]'); - await expect(rows.nth(2)).toBeVisible(); - - const firstRow = rows.nth(1); - await expect(firstRow.locator('[role="rowheader"]')).toContainText('Console'); - await firstRow.click(); - await page.getByRole('tab', { name: 'Output' }).click(); - const firstOutput = await readFromCodeEditor(page, 'Execution Output'); - expect(firstOutput).toContain('Setup complete!'); - - await page.goto('/acm#/history'); - await expect(grid).toBeVisible(); - - const secondRow = rows.nth(2); - await expect(secondRow.locator('[role="rowheader"]')).toContainText('Console'); - await secondRow.click(); - await page.getByRole('tab', { name: 'Output' }).click(); - const secondOutput = await readFromCodeEditor(page, 'Execution Output'); - expect(secondOutput).toContain('Hello World!'); - }); - - test('Shows automatic script executions', async ({ page }) => { - await page.goto('/acm'); - await page.getByRole('button', { name: 'History' }).click(); - - const grid = page.locator('[role="grid"][aria-label="Executions table"]'); - await expect(grid).toBeVisible(); - const rows = grid.locator('[role="row"]'); - - await page.getByRole('searchbox', { name: 'Executable' }).fill('example/ACME-20_once'); - await expect(rows.nth(1)).toContainText('Script \'example/ACME-20_once\''); - await expect(rows.nth(1)).toContainText('succeeded'); - - await page.getByRole('searchbox', { name: 'Executable' }).fill('example/ACME-21_changed'); - await expect(rows.nth(1)).toContainText('Script \'example/ACME-21_changed\''); - await expect(rows.nth(1)).toContainText('succeeded'); - }); -}); diff --git a/test/e2e/005-manual-scripts.spec.ts b/test/e2e/005-manual-scripts.spec.ts index 821a0c87..3fc06770 100644 --- a/test/e2e/005-manual-scripts.spec.ts +++ b/test/e2e/005-manual-scripts.spec.ts @@ -9,28 +9,38 @@ import { expectOutputFileDownload, } from './utils/expect'; import { readFromCodeEditor } from './utils/editor'; +import { attachScreenshot } from './utils/page'; test.describe('Manual Scripts', () => { - test('Execute CSV Generation With I/O', async ({ page }) => { + test('Execute CSV Generation', async ({ page }, testInfo) => { await page.goto('/acm'); await page.getByRole('button', { name: 'Scripts' }).click(); + await expect(page.locator('[role="grid"][aria-label="Script list (manual)"]')).toBeVisible(); + await page.getByText('example/ACME-203_output-csv').click(); + await expect(page).toHaveURL(/\/acm#\/scripts\/view\/%2Fconf%2Facm%2Fsettings%2Fscript%2Fmanual%2Fexample%2FACME-203_output-csv\.groovy/); + await page.waitForTimeout(1000); + await attachScreenshot(page, testInfo, 'Script Details'); + await page.getByRole('button', { name: 'Execute' }).click(); await page.getByRole('textbox', { name: 'Users to' }).fill('5000'); await page.getByRole('textbox', { name: 'First names' }).fill('John\nJane\nJack\nAlice\nBob\nRobert'); await page.getByRole('textbox', { name: 'Last names' }).fill('Doe\nSmith\nBrown\nJohnson\nWhite\nJordan'); + // TODO await attachScreenshot(page, testInfo, 'Inputs Dialog'); + await page.getByRole('button', { name: 'Start' }).click(); await expectExecutionProgressBarSucceeded(page); const output = await readFromCodeEditor(page, 'Execution Output'); expect(output).toContain('[SUCCESS] Users CSV report generation ended successfully'); + await attachScreenshot(page, testInfo, 'Execution Console Output'); await page.getByRole('tab', { name: 'Details' }).click(); await page.waitForTimeout(1000); - await page.screenshot(); + await attachScreenshot(page, testInfo, 'Execution Details'); await expectExecutionDetails(page); await expectExecutionTimings(page); @@ -56,14 +66,16 @@ test.describe('Manual Scripts', () => { ]); await page.getByRole('tab', { name: 'Output' }).click(); - await page.getByRole('button', { name: 'Review' }).click(); + await page.getByRole('tab', { name: 'Texts' }).click(); await expectOutputTexts(page, ['Processed 5000 user(s)']); + // TODO await attachScreenshot(page, testInfo, 'Outputs Review - Texts'); await page.getByTestId('modal').getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Review' }).click(); await page.getByRole('tab', { name: 'Files' }).click(); + // TODO await attachScreenshot(page, testInfo, 'Outputs Review - Files'); await expectOutputFileDownload(page, 'Download Archive', /\.(zip)$/); await page.getByRole('button', { name: 'Review' }).click(); diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index ac77d64b..a0651bad 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from '@playwright/test'; +import { authHeader, BASE_URL } from './utils/env'; /** * Read environment variables from file. @@ -26,21 +27,22 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('')`. */ - baseURL: 'http://localhost:5502', + baseURL: BASE_URL, /* Action timeout (e.g. toBeEnabled, toBeVisible) */ actionTimeout: 10000, /* Basic Auth for AEM */ extraHTTPHeaders: { - 'Authorization': 'Basic ' + Buffer.from('admin:admin').toString('base64'), + ...authHeader(), + 'Origin': BASE_URL, }, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - /* Screenshots on failure */ - screenshot: 'on', + /* Screenshots only on failure (manual screenshots still work) */ + screenshot: 'only-on-failure', /* Videos on failure */ video: 'retain-on-failure', diff --git a/test/e2e/utils/context.ts b/test/e2e/utils/context.ts index 116d10b1..0f57c8e4 100644 --- a/test/e2e/utils/context.ts +++ b/test/e2e/utils/context.ts @@ -1,4 +1,5 @@ import { Browser, Page } from '@playwright/test'; +import { authHeader, BASE_URL } from './env'; export async function newAemContext( browser: Browser, @@ -7,10 +8,8 @@ export async function newAemContext( callback: (page: Page) => Promise ): Promise { const context = await browser.newContext({ - baseURL: 'http://localhost:5502', - extraHTTPHeaders: { - 'Authorization': 'Basic ' + btoa(`${user}:${password}`), - }, + baseURL: BASE_URL, + extraHTTPHeaders: authHeader(user, password), }); const page = await context.newPage(); diff --git a/test/e2e/utils/env.ts b/test/e2e/utils/env.ts new file mode 100644 index 00000000..db4dcb50 --- /dev/null +++ b/test/e2e/utils/env.ts @@ -0,0 +1,42 @@ +import { test, Page } from '@playwright/test'; + +export const testOnEnv = (env: string) => { + return process.env.AEM_ENV === env ? test : test.skip; +}; + +export const BASE_URL = 'http://localhost:5502'; +export const ADMIN_USER = 'admin'; +export const ADMIN_PASSWORD = 'admin'; + +export const authHeader = (user = ADMIN_USER, password = ADMIN_PASSWORD) => ({ + 'Authorization': 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64') +}); + +export const baseUrlHeaders = () => ({ + 'Origin': BASE_URL, + 'Referer': BASE_URL, +}); + +export const csrfHeader = (token: string) => ({ + 'CSRF-Token': token, +}); + +export const getCsrfToken = async (page: Page): Promise => { + const tokenResponse = await page.request.get('/libs/granite/csrf/token.json', { + headers: { + ...authHeader(), + ...baseUrlHeaders(), + }, + }); + const tokenData = await tokenResponse.json(); + return tokenData.token; +}; + +export const apiHeaders = async (page: Page) => { + const csrfToken = await getCsrfToken(page); + return { + ...authHeader(), + ...baseUrlHeaders(), + ...csrfHeader(csrfToken), + }; +}; diff --git a/test/e2e/utils/page.ts b/test/e2e/utils/page.ts new file mode 100644 index 00000000..552f8f6d --- /dev/null +++ b/test/e2e/utils/page.ts @@ -0,0 +1,6 @@ +import { Page, TestInfo } from '@playwright/test'; + +export async function attachScreenshot(page: Page, testInfo: TestInfo, name: string): Promise { + const screenshot = await page.screenshot(); + await testInfo.attach(name, { body: screenshot, contentType: 'image/png' }); +}