From de2e1957cbc4c5fb369ebd4fc080a815df0eee55 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 5 Sep 2024 23:58:12 +0200 Subject: [PATCH] Page objects. --- tools/e2e/tests/_fixture.ts | 61 +++-- tools/e2e/tests/given-app/_setup.ts | 15 +- tools/e2e/tests/given-app/rules.spec.ts | 92 +++---- tools/e2e/tests/given-app/schemas.spec.ts | 227 ++++++++-------- tools/e2e/tests/given-login/_setup.ts | 23 +- tools/e2e/tests/given-login/apps-page.spec.ts | 4 +- tools/e2e/tests/given-login/apps.spec.ts | 13 +- tools/e2e/tests/given-schema/_setup.ts | 33 ++- tools/e2e/tests/given-schema/contents.spec.ts | 253 +++++++++--------- ...start-page.spec.ts => login-start.spec.ts} | 6 +- tools/e2e/tests/login.spec.ts | 35 +-- tools/e2e/tests/pages/apps.ts | 38 +++ tools/e2e/tests/pages/content.ts | 76 ++++++ tools/e2e/tests/pages/contents.ts | 63 +++++ tools/e2e/tests/pages/dropdown.ts | 27 ++ tools/e2e/tests/pages/index.ts | 16 ++ tools/e2e/tests/pages/login.ts | 47 ++++ tools/e2e/tests/pages/rule.ts | 30 +++ tools/e2e/tests/pages/rules.ts | 59 ++++ tools/e2e/tests/pages/schema.ts | 108 ++++++++ tools/e2e/tests/pages/schemas.ts | 48 ++++ tools/e2e/tests/utils.ts | 69 ----- 22 files changed, 884 insertions(+), 459 deletions(-) rename tools/e2e/tests/{start-page.spec.ts => login-start.spec.ts} (72%) create mode 100644 tools/e2e/tests/pages/apps.ts create mode 100644 tools/e2e/tests/pages/content.ts create mode 100644 tools/e2e/tests/pages/contents.ts create mode 100644 tools/e2e/tests/pages/dropdown.ts create mode 100644 tools/e2e/tests/pages/index.ts create mode 100644 tools/e2e/tests/pages/login.ts create mode 100644 tools/e2e/tests/pages/rule.ts create mode 100644 tools/e2e/tests/pages/rules.ts create mode 100644 tools/e2e/tests/pages/schema.ts create mode 100644 tools/e2e/tests/pages/schemas.ts diff --git a/tools/e2e/tests/_fixture.ts b/tools/e2e/tests/_fixture.ts index 3855e697d2..1f4237f5a2 100644 --- a/tools/e2e/tests/_fixture.ts +++ b/tools/e2e/tests/_fixture.ts @@ -5,34 +5,45 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { test as base, Page } from '@playwright/test'; - -type BaseFixture = { - dropdown: Dropdown; +import { test as base } from '@playwright/test'; +import { ContentPage, ContentsPage, LoginPage, RulePage, RulesPage, SchemaPage, SchemasPage } from './pages'; +import { AppsPage } from './pages/apps'; + +export type BaseFixture = { + appsPage: AppsPage; + contentPage: ContentPage; + contentsPage: ContentsPage; + loginPage: LoginPage; + rulePage: RulePage; + rulesPage: RulesPage; + schemaPage: SchemaPage; + schemasPage: SchemasPage; }; -class Dropdown { - constructor( - private readonly page: Page, - ) { - } - - public async delete() { - await this.page.getByText('Delete').click(); - await this.page.getByRole('button', { name: /Yes/ }).click(); - } - - public async action(name: string) { - await this.page.getByText(name).click(); - await this.page.locator('sqx-dropdown-menu').waitFor({ state: 'hidden' }); - } -} - export const test = base.extend({ - dropdown: async ({ page }, use) => { - const dropdown = new Dropdown(page); - - await use(dropdown); + appsPage: async ({ page }, use) => { + await use(new AppsPage(page)); + }, + contentPage: async ({ page }, use) => { + await use(new ContentPage(page)); + }, + contentsPage: async ({ page }, use) => { + await use(new ContentsPage(page)); + }, + loginPage: async ({ page }, use) => { + await use(new LoginPage(page)); + }, + rulePage: async ({ page }, use) => { + await use(new RulePage(page)); + }, + rulesPage: async ({ page }, use) => { + await use(new RulesPage(page)); + }, + schemaPage: async ({ page }, use) => { + await use(new SchemaPage(page)); + }, + schemasPage: async ({ page }, use) => { + await use(new SchemasPage(page)); }, }); diff --git a/tools/e2e/tests/given-app/_setup.ts b/tools/e2e/tests/given-app/_setup.ts index 9fdf91d653..cbe2bdd386 100644 --- a/tools/e2e/tests/given-app/_setup.ts +++ b/tools/e2e/tests/given-app/_setup.ts @@ -5,20 +5,19 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { test as setup } from '@playwright/test'; import { getRandomId, writeJsonAsync } from '../utils'; +import { test as setup } from './_fixture'; -setup('prepare app', async ({ page }) => { +setup('prepare app', async ({ appsPage }) => { const appName = `my-app-${getRandomId()}`; - await page.goto('/app'); + await appsPage.goto(); - await page.getByTestId('new-app').click(); + const dialog = await appsPage.openAppDialog(); + await dialog.enterName(appName); + await dialog.save(); - await page.locator('#name').fill(appName); - await page.getByRole('button', { name: 'Create' }).click(); - - await page.getByRole('heading', { name: appName }).click(); + await appsPage.gotoApp(appName); await writeJsonAsync('app', { appName }); }); \ No newline at end of file diff --git a/tools/e2e/tests/given-app/rules.spec.ts b/tools/e2e/tests/given-app/rules.spec.ts index 14598dd4dc..d1d6c58655 100644 --- a/tools/e2e/tests/given-app/rules.spec.ts +++ b/tools/e2e/tests/given-app/rules.spec.ts @@ -1,90 +1,80 @@ -import { expect, Page } from '@playwright/test'; -import { escapeRegex, getRandomId } from '../utils'; +import { expect } from '@playwright/test'; +import { RulePage, RulesPage } from '../pages'; +import { getRandomId } from '../utils'; import { test } from './_fixture'; // We have no easy way to identity rules. Therefore run them sequentially. test.describe.configure({ mode: 'serial' }); -test.beforeEach(async ({ page, appName }) => { - await page.goto(`/app/${appName}/rules`); +test.beforeEach(async ({ appName, rulesPage }) => { + await rulesPage.goto(appName); }); -test('create rule', async ({ page }) => { - const ruleName = await createRandomRule(page); - const ruleCard = page.locator('div.card', { hasText: escapeRegex(ruleName) }); +test('create rule', async ({ rulesPage, rulePage }) => { + const ruleName = await createRandomRule(rulesPage, rulePage); + const ruleCard = await rulesPage.getRule(ruleName); - await expect(ruleCard).toBeVisible(); + await expect(ruleCard.root).toBeVisible(); }); -test('delete rule', async ({ dropdown, page }) => { - const ruleName = await createRandomRule(page); - const ruleCard = page.locator('div.card', { hasText: escapeRegex(ruleName) }); +test('delete rule', async ({ rulesPage, rulePage }) => { + const ruleName = await createRandomRule(rulesPage, rulePage); + const ruleCard = await rulesPage.getRule(ruleName); - await ruleCard.getByLabel('Options').click(); + const dropdown = await ruleCard.openOptionsDropdown(); await dropdown.delete(); - await expect(ruleCard).not.toBeVisible(); + await expect(ruleCard.root).toBeVisible(); }); -test('disable rule', async ({ dropdown, page }) => { - const ruleName = await createRandomRule(page); - const ruleCard = page.locator('div.card', { hasText: escapeRegex(ruleName) }); +test('disable rule', async ({ rulePage, rulesPage }) => { + const ruleName = await createRandomRule(rulesPage, rulePage); + const ruleCard = await rulesPage.getRule(ruleName); - await ruleCard.getByLabel('Options').click(); + const dropdown = await ruleCard.openOptionsDropdown(); await dropdown.action('Disable'); - await expect(ruleCard.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'unchecked'); + await expect(ruleCard.root.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'unchecked'); }); -test('enable rule', async ({ dropdown, page }) => { - const ruleName = await createRandomRule(page); - const ruleCard = page.locator('div.card', { hasText: escapeRegex(ruleName) }); +test('enable rule', async ({ rulePage, rulesPage }) => { + const ruleName = await createRandomRule(rulesPage, rulePage); + const ruleCard = await rulesPage.getRule(ruleName); - await ruleCard.getByLabel('Options').click(); - await dropdown.action('Disable'); + const dropdown1 = await ruleCard.openOptionsDropdown(); + await dropdown1.action('Disable'); - await expect(ruleCard.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'unchecked'); + await expect(ruleCard.root.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'unchecked'); - await ruleCard.getByLabel('Options').click(); - await dropdown.action('Enable'); + const dropdown2 = await ruleCard.openOptionsDropdown(); + await dropdown2.action('Enable'); - await expect(ruleCard.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'checked'); + await expect(ruleCard.root.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'checked'); }); -test('edit rule', async ({ dropdown, page }) => { - const ruleName = await createRandomRule(page); - const ruleCard = page.locator('div.card', { hasText: escapeRegex(ruleName) }); +test('edit rule', async ({ page, rulePage, rulesPage }) => { + const ruleName = await createRandomRule(rulesPage, rulePage); + const ruleCard = await rulesPage.getRule(ruleName); - await ruleCard.getByLabel('Options').click(); + const dropdown = await ruleCard.openOptionsDropdown(); await dropdown.action('Edit'); await expect(page.getByText('Enabled')).toBeVisible(); }); -async function createRandomRule(page: Page) { +async function createRandomRule(rulesPage: RulesPage, rulePage: RulePage) { const ruleName = `rule-${getRandomId()}`; - await page.getByRole('link', { name: /New Rule/ }).click(); - - // Define rule action - await page.getByText('Content changed').click(); - - // Define rule trigger - await page.getByText('Webhook').click(); - // This is the only required field, so we have to enter some text. - await page.locator('sqx-formattable-input').first().getByRole('textbox').fill('https:/squidex.io'); - - await page.getByRole('button', { name: 'Save' }).click(); - - await page.getByText('Enabled').waitFor({ state: 'visible' }); + await rulesPage.addRule(); - // Go back - await page.getByLabel('Back').click(); + await rulePage.selectContentChangedTrigger(); + await rulePage.selectWebhookAction(); + await rulePage.save(); + await rulePage.back(); - // Define rule name. - await page.locator('div.card', { hasText: /Unnamed Rule/ }).getByRole('heading').first().dblclick(); - await page.locator('form').getByRole('textbox').fill(ruleName); - await page.locator('form').getByLabel('Save').click(); + const rename = await rulesPage.renameRule(/Unnamed Rule/); + await rename.enterName(ruleName); + await rename.save(); return ruleName; } \ No newline at end of file diff --git a/tools/e2e/tests/given-app/schemas.spec.ts b/tools/e2e/tests/given-app/schemas.spec.ts index 34dd03ab74..18d305b9ce 100644 --- a/tools/e2e/tests/given-app/schemas.spec.ts +++ b/tools/e2e/tests/given-app/schemas.spec.ts @@ -5,180 +5,193 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Page } from '@playwright/test'; -import { createField, createNestedField, createSchema, escapeRegex, FieldSaveMode, getRandomId, saveField } from '../utils'; +import { SchemaPage, SchemasPage } from '../pages'; +import { getRandomId } from '../utils'; import { expect, test } from './_fixture'; -test.beforeEach(async ({ page, appName }) => { - await page.goto(`/app/${appName}/schemas`); +test.beforeEach(async ({ appName, schemasPage }) => { + await schemasPage.goto(appName); }); -test('create schema', async ({ page }) => { - // Add schema. - const schemaName = await createRandomSchema(page); - const schemaLink = page.locator('a.nav-link', { hasText: escapeRegex(schemaName) }); +test('create schema', async ({ schemasPage }) => { + const schemaName = await createRandomSchema(schemasPage); + const schemaLink = await schemasPage.getSchemaLink(schemaName); - await expect(schemaLink).toBeVisible(); + await expect(schemaLink.root).toBeVisible(); }); -test('delete schema', async ({ dropdown, page }) => { - // Add schema. - const schemaName = await createRandomSchema(page); - const schemaLink = page.locator('a.nav-link', { hasText: escapeRegex(schemaName) }); +test('delete schema', async ({ schemasPage, schemaPage }) => { + const schemaName = await createRandomSchema(schemasPage); + const schemaLink = await schemasPage.getSchemaLink(schemaName); - // Delete schema. - await page.getByLabel('Options').click(); + const dropdown = await schemaPage.openOptionsDropdown(); await dropdown.delete(); - await expect(schemaLink).not.toBeVisible(); + await expect(schemaLink.root).not.toBeVisible(); }); -test('publish schema', async ({ page }) => { - // Add schema. - await createRandomSchema(page); +test('publish schema', async ({ schemasPage, schemaPage }) => { + await createRandomSchema( schemasPage); - // Publish schema. - await page.getByRole('button', { name: 'Published', exact: true }).click(); + await schemaPage.publish(); +}); + +test('unpublish schema', async ({ schemasPage, schemaPage }) => { + await createRandomSchema( schemasPage); - await expect(page.getByRole('button', { name: 'Published', exact: true })).toBeDisabled(); + await schemaPage.publish(); + await schemaPage.unpublish(); }); -test('add field', async ({ page }) => { - // Add schema. - await createRandomSchema(page); +test('add field', async ({ schemasPage, schemaPage }) => { + await createRandomSchema( schemasPage); - const fieldName = await createRandomField(page, 'CreateAndClose'); - const fieldRow = page.getByText(fieldName); + const fieldName = await createRandomField(schemaPage); + const fieldRow = await schemaPage.getFieldRow(fieldName); - await expect(fieldRow).toBeVisible(); + await expect(fieldRow.root).toBeVisible(); }); -test('add field and edit', async ({ page }) => { - const fieldLabel = `field-${getRandomId()}`; +test('add field and edit', async ({ schemasPage, schemaPage }) => { + await createRandomSchema( schemasPage); - // Add schema. - await createRandomSchema(page); + const fieldName = `field-${getRandomId()}`; + const fieldLabel = `field-${getRandomId()}`; - // Add field. - await createRandomField(page, 'CreateAndEdit'); + const fieldDialog = await schemaPage.addField(); + await fieldDialog.enterName(fieldName); + await fieldDialog.createAndEdit(); - // Edit field. - await page.getByLabel('Label').fill(fieldLabel); - await saveField(page, 'SaveAndClose'); + await fieldDialog.enterLabel(fieldLabel); + await fieldDialog.saveAndClose(); - const fieldRow = page.getByText(fieldLabel); + const fieldRow = await schemaPage.getFieldRow(fieldLabel); - await expect(fieldRow).toBeVisible(); + await expect(fieldRow.root).toBeVisible(); }); -test('add field and add another', async ({ page }) => { - // Add schema. - await createRandomSchema(page); +test('add field and add another', async ({ schemasPage, schemaPage }) => { + await createRandomSchema( schemasPage); - // Add field. - const fieldName1 = await createRandomField(page, 'CreateAndAdd'); + const fieldName1 = `field-${getRandomId()}`; const fieldName2 = `field-${getRandomId()}`; - // Add another field. - await page.getByPlaceholder('Enter field name').fill(fieldName2); - await saveField(page, 'CreateAndClose'); + const fieldDialog = await schemaPage.addField(); + await fieldDialog.enterName(fieldName1); + await fieldDialog.createAndAdd(); + + await fieldDialog.enterName(fieldName2); + await fieldDialog.createAndAdd(); - const fieldRow1 = page.getByText(fieldName1); - const fieldRow2 = page.getByText(fieldName2); + const fieldRow1 = await schemaPage.getFieldRow(fieldName1); + const fieldRow2 = await schemaPage.getFieldRow(fieldName2); - await expect(fieldRow1).toBeVisible(); - await expect(fieldRow2).toBeVisible(); + await expect(fieldRow1.root).toBeVisible(); + await expect(fieldRow2.root).toBeVisible(); }); -test('add field to array', async ({ page }) => { - // Add schema. - await createRandomSchema(page); +test('add field to array', async ({ schemasPage, schemaPage }) => { + await createRandomSchema( schemasPage); - // Add array array. - await createRandomField(page, 'CreateAndClose', 'Array'); + const rootFieldName = `field-${getRandomId()}`; - // Add field to array. - const fieldName = await createRandomNestedField(page, 'CreateAndClose'); - const fieldRow = page.getByText(fieldName); + const rootDialog = await schemaPage.addField(); + await rootDialog.enterName(rootFieldName); + await rootDialog.enterType('Array'); + await rootDialog.createAndClose(); - await expect(fieldRow).toBeVisible(); + const nestedFieldName = `field-${getRandomId()}`; + + const nestedDialog = await schemaPage.addNestedField(); + await nestedDialog.enterName(nestedFieldName); + await nestedDialog.createAndClose(); + + const fieldRow = await schemaPage.getFieldRow(nestedFieldName); + + await expect(fieldRow.root).toBeVisible(); }); -test('add field to array and ed', async ({ page }) => { - const fieldLabel = `field-${getRandomId()}`; +test('add field to array and ed', async ({ schemasPage, schemaPage }) => { + await createRandomSchema(schemasPage); - // Add schema. - await createRandomSchema(page); + const rootFieldName = `field-${getRandomId()}`; - // Add array array. - await createRandomField(page, 'CreateAndClose', 'Array'); + const rootDialog = await schemaPage.addField(); + await rootDialog.enterName(rootFieldName); + await rootDialog.enterType('Array'); + await rootDialog.createAndClose(); - // Add field to array. - await createRandomNestedField(page, 'CreateAndEdit'); + const nestedFieldName = `field-${getRandomId()}`; + const nestedFieldLabel = `field-${getRandomId()}`; - // Edit field. - await page.getByLabel('Label').fill(fieldLabel); - await saveField(page, 'SaveAndClose'); + const nestedDialog = await schemaPage.addNestedField(); + await nestedDialog.enterName(nestedFieldName); + await nestedDialog.createAndEdit(); - const fieldRow = page.getByText(fieldLabel); + await nestedDialog.enterLabel(nestedFieldLabel); + await nestedDialog.saveAndClose(); - await expect(fieldRow).toBeVisible(); + const fieldRow = await schemaPage.getFieldRow(nestedFieldLabel); + + await expect(fieldRow.root).toBeVisible(); }); -test('add field to array and another', async ({ page }) => { - // Add schema. - await createRandomSchema(page); +test('add field to array and another', async ({ schemasPage, schemaPage }) => { + await createRandomSchema(schemasPage); - // Add array array. - await createRandomField(page, 'CreateAndClose', 'Array'); + const rootFieldName = `field-${getRandomId()}`; - // Add field to array. - const fieldName1 = await createRandomNestedField(page, 'CreateAndAdd'); - const fieldName2 = `field-${getRandomId()}`; + const rootDialog = await schemaPage.addField(); + await rootDialog.enterName(rootFieldName); + await rootDialog.enterType('Array'); + await rootDialog.createAndClose(); + + const nestedFieldName1 = `field-${getRandomId()}`; + const nestedFieldName2 = `field-${getRandomId()}`; - // Add another field. - await page.getByPlaceholder('Enter field name').fill(fieldName2); - await saveField(page, 'CreateAndClose'); + const nestedDialog = await schemaPage.addNestedField(); + await nestedDialog.enterName(nestedFieldName1); + await nestedDialog.createAndAdd(); - const fieldRow1 = page.getByText(fieldName1); - const fieldRow2 = page.getByText(fieldName2); + await nestedDialog.enterName(nestedFieldName2); + await nestedDialog.createAndClose(); - await expect(fieldRow1).toBeVisible(); - await expect(fieldRow2).toBeVisible(); + const fieldRow1 = await schemaPage.getFieldRow(nestedFieldName1); + const fieldRow2 = await schemaPage.getFieldRow(nestedFieldName2); + + await expect(fieldRow1.root).toBeVisible(); + await expect(fieldRow2.root).toBeVisible(); }); -test('delete field', async ({ dropdown, page }) => { - // Add schema. - await createRandomSchema(page); +test('delete field', async ({ schemasPage, schemaPage }) => { + await createRandomSchema(schemasPage); - // Add field. - const fieldName = await createRandomField(page, 'CreateAndClose'); - const fieldRow = page.locator('div.table-items-row-summary', { hasText: escapeRegex(fieldName) }); + const fieldName = await createRandomField(schemaPage); + const fieldRow = await schemaPage.getFieldRow(fieldName); - // Delete field. - await fieldRow.getByLabel('Options').click(); + const dropdown = await fieldRow.openOptionsDropdown(); await dropdown.delete(); - await expect(fieldRow).not.toBeVisible(); + await expect(fieldRow.root).not.toBeVisible(); }); -async function createRandomField(page: Page, mode: FieldSaveMode, type = 'String') { +async function createRandomField(schemaPage: SchemaPage) { const name = `field-${getRandomId()}`; - await createField(page, { name, mode, type }); - return name; -} - -async function createRandomNestedField(page: Page, mode: FieldSaveMode, type = 'String') { - const name = `field-${getRandomId()}`; + const fieldDialog = await schemaPage.addField(); + await fieldDialog.enterName(name); + await fieldDialog.enterType('String'); + await fieldDialog.createAndClose(); - await createNestedField(page, { name, mode, type }); return name; } -async function createRandomSchema(page: Page) { +async function createRandomSchema(schemasPage: SchemasPage) { const name = `schema-${getRandomId()}`; - await createSchema(page, { name }); + const schemaDialog = await schemasPage.createSchema(); + await schemaDialog.enterName(name); + await schemaDialog.save(); + return name; } \ No newline at end of file diff --git a/tools/e2e/tests/given-login/_setup.ts b/tools/e2e/tests/given-login/_setup.ts index f4c9a57896..b03e98b9f4 100644 --- a/tools/e2e/tests/given-login/_setup.ts +++ b/tools/e2e/tests/given-login/_setup.ts @@ -8,22 +8,13 @@ import { STORAGE_STATE } from '../../playwright.config'; import { test as setup } from './_fixture'; -setup('prepare login', async ({ page, userEmail, userPassword }) => { - await page.goto('/'); - - // Start waiting for popup before clicking. - const popupPromise = page.waitForEvent('popup'); - - await page.getByTestId('login').click(); - - const popup = await popupPromise; - await popup.waitForLoadState(); - - await popup.getByTestId('login-button').waitFor(); - - await popup.getByPlaceholder('Enter Email').fill(userEmail); - await popup.getByPlaceholder('Enter Password').fill(userPassword); - await popup.getByTestId('login-button').click(); +setup('prepare login', async ({ page, userEmail, userPassword, loginPage }) => { + await loginPage.goto(); + + const popup = await loginPage.openPopup(); + await popup.enterEmail(userEmail); + await popup.enterPassword('1q2w3e$R'), + await popup.login(); await page.waitForURL(/app/); diff --git a/tools/e2e/tests/given-login/apps-page.spec.ts b/tools/e2e/tests/given-login/apps-page.spec.ts index 0857d2b5e0..7d1751c915 100644 --- a/tools/e2e/tests/given-login/apps-page.spec.ts +++ b/tools/e2e/tests/given-login/apps-page.spec.ts @@ -7,8 +7,8 @@ import { expect, test } from './_fixture'; -test.beforeEach(async ({ page }) => { - await page.goto('/app'); +test.beforeEach(async ({ appsPage }) => { + await appsPage.goto(); }); test('has title', async ({ page }) => { diff --git a/tools/e2e/tests/given-login/apps.spec.ts b/tools/e2e/tests/given-login/apps.spec.ts index 6e14ac7b86..7dc8c40285 100644 --- a/tools/e2e/tests/given-login/apps.spec.ts +++ b/tools/e2e/tests/given-login/apps.spec.ts @@ -8,17 +8,16 @@ import { getRandomId } from '../utils'; import { expect, test } from './_fixture'; -test.beforeEach(async ({ page }) => { - await page.goto('/app'); +test.beforeEach(async ({ appsPage }) => { + await appsPage.goto(); }); -test('create app', async ({ page }) => { +test('create app', async ({ page, appsPage }) => { const appName = `my-app-${getRandomId()}`; - await page.getByTestId('new-app').click(); - - await page.locator('#name').fill(appName); - await page.getByRole('button', { name: 'Create' }).click(); + const appDialog = await appsPage.openAppDialog(); + await appDialog.enterName(appName); + await appDialog.save(); const newApp = page.getByRole('heading', { name: appName }); diff --git a/tools/e2e/tests/given-schema/_setup.ts b/tools/e2e/tests/given-schema/_setup.ts index 5925eb01a9..73ccd6c042 100644 --- a/tools/e2e/tests/given-schema/_setup.ts +++ b/tools/e2e/tests/given-schema/_setup.ts @@ -5,32 +5,31 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { expect, test as setup } from '../given-app/_fixture'; -import { createField, createSchema, getRandomId, writeJsonAsync } from '../utils'; +import { test as setup } from '../given-app/_fixture'; +import { getRandomId, writeJsonAsync } from '../utils'; -setup('prepare schema', async ({ page, appName }) => { +const fields = [{ + name: 'my-string', +}]; + +setup('prepare schema', async ({ appName, schemasPage, schemaPage }) => { const schemaName = `my-schema-${getRandomId()}`; - const fields = [{ - name: 'my-string', - }]; + await schemasPage.goto(appName); - await page.goto(`/app/${appName}/schemas`); + const createDialog = await schemasPage.createSchema(); + await createDialog.enterName(schemaName); + await createDialog.save(); - // Add schema. - await createSchema(page, { name: schemaName }); + await schemaPage.publish(); - // Add fields. for (const field of fields) { - await createField(page, { name: field.name }); + const fieldDialog = await schemaPage.addField(); + await fieldDialog.enterName(field.name); + await fieldDialog.enterType('String'); + await fieldDialog.createAndClose(); } - // Publish schema. - await page.getByRole('button', { name: 'Published', exact: true }).click(); - - // Just wait for the publish operation to complete. - await expect(page.getByRole('button', { name: 'Published', exact: true })).toBeDisabled(); - await writeJsonAsync('schema', { schemaName }); await writeJsonAsync('fields', { fields }); }); \ No newline at end of file diff --git a/tools/e2e/tests/given-schema/contents.spec.ts b/tools/e2e/tests/given-schema/contents.spec.ts index 900efb1dac..c2fc02f30a 100644 --- a/tools/e2e/tests/given-schema/contents.spec.ts +++ b/tools/e2e/tests/given-schema/contents.spec.ts @@ -5,206 +5,199 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Page } from '@playwright/test'; -import { escapeRegex, getRandomId } from '../utils'; +import { ContentPage, ContentsPage } from '../pages'; +import { getRandomId } from '../utils'; import { expect, test } from './_fixture'; -test.beforeEach(async ({ page, appName, schemaName }) => { - await page.goto(`/app/${appName}/content/${schemaName}`); - await page.getByRole('combobox').selectOption('3: 50'); +test.beforeEach(async ({ appName, schemaName, contentsPage }) => { + await contentsPage.goto(appName, schemaName); + await contentsPage.increasePageSize(); }); -test('create content and close', async ({ page }) => { - const contentText = await createRandomContent(page, 'SaveAndClose'); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); +test('create content and close', async ({ contentsPage, contentPage }) => { + await contentsPage.addContent(); - await expect(contentRow).toBeVisible(); + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); + await contentPage.saveAndClose(); + + const contentRow = await contentsPage.getContentRow(contentText); + + await expect(contentRow.root.getByLabel('Draft')).toBeVisible(); }); -test('create content and edit', async ({ page }) => { - await createRandomContent(page, 'SaveAndEdit'); +test('create content and edit', async ({ page, contentsPage, contentPage }) => { + await contentsPage.addContent(); + + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); + await contentPage.saveAndEdit(); await expect(page.getByRole('button', { name: /Draft/ })).toBeVisible(); await expect(page.getByLabel('Identity')).toBeVisible(); }); -test('create content and add another', async ({ page }) => { - await createRandomContent(page, 'SaveAndAdd'); +test('create content and add another', async ({ page, contentsPage, contentPage }) => { + await contentsPage.addContent(); + + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); + await contentPage.saveAndAdd(); await expect(page.locator('sqx-field-editor').getByRole('textbox')).toBeEmpty(); }); -test('create content as published and edit', async ({ page }) => { - await createRandomContent(page, 'SavePublishAndEdit'); +test('create content as published and edit', async ({ page, contentsPage, contentPage }) => { + await contentsPage.addContent(); + + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); + await contentPage.savePublishAndEdit(); await expect(page.getByRole('button', { name: /Published/ })).toBeVisible(); await expect(page.getByLabel('Identity')).toBeVisible(); }); -test('create content as published and close', async ({ page }) => { - const contentText = await createRandomContent(page, 'SavePublishAndClose'); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); +test('create content as published and close', async ({ contentsPage, contentPage }) => { + await contentsPage.addContent(); - await expect(contentRow.getByLabel('Published')).toBeVisible(); -}); + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); + await contentPage.savePublishAndClose(); -test('update content', async ({ page }) => { - const contentText = await createRandomContent(page, 'SaveAndClose'); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); + const contentRow = await contentsPage.getContentRow(contentText); - await contentRow.click(); + await expect(contentRow.root.getByLabel('Published')).toBeVisible(); +}); - const contentUpdate = `content-${getRandomId()}`; +test('update content', async ({ page, contentsPage, contentPage }) => { + const contentText = await createRandomContent(contentsPage, contentPage); + const contentRow = await contentsPage.getContentRow(contentText); + await contentRow.edit(); - // Define content value. - await page.locator('sqx-field-editor').getByRole('textbox').fill(contentUpdate); - await saveContent(page, 'Save'); + const contentUpdate = `content-${getRandomId()}`; - await page.getByRole('button', { name: 'Save', exact: true }).click(); + await contentPage.enterField(contentUpdate); + await contentPage.save(); - // Wait for update of the version await page.getByText('Version: 1').waitFor({ state: 'visible' }); + await contentPage.back(); - // Go back - await page.getByLabel('Back').click(); - - const updateRow = page.locator('tr', { hasText: escapeRegex(contentUpdate) }); + const updateRow = await contentsPage.getContentRow(contentUpdate); - await expect(updateRow).toBeVisible(); + await expect(updateRow.root).toBeVisible(); }); const states = [{ - state: 'Archived', - currentState: 'Draft', - initialPublished: false, + status: 'Archived', + currentStatus: 'Draft', }, { - state: 'Draft', - currentState: 'Published', - initialPublished: true, + status: 'Draft', + currentStatus: 'Published', }, { - state: 'Published', - currentState: 'Draft', - initialPublished: false, + status: 'Published', + currentStatus: 'Draft', }]; -states.forEach(({ state, currentState, initialPublished }) => { - const mode: SaveMode = initialPublished ? 'SavePublishAndClose' : 'SaveAndClose'; +states.forEach(({ status, currentStatus }) => { + test(`change content from <${currentStatus}> to <${status}>`, async ({ contentsPage, contentPage }) => { + await contentsPage.addContent(); + + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); - test(`change content to ${state}`, async ({ dropdown, page }) => { - const contentText = await createRandomContent(page, mode); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); + if (currentStatus === 'Published') { + await contentPage.savePublishAndClose(); + } else { + await contentPage.saveAndClose(); + } - await contentRow.getByLabel('Options').click(); - await dropdown.action(`Change to ${state}`); - await page.getByRole('button', { name: 'Confirm' }).click(); + const contentRow = await contentsPage.getContentRow(contentText); + const dropdown = await contentRow.openOptionsDropdown(); + await dropdown.actionConfirm(`Change to ${status}`); - await expect(contentRow.getByLabel(state)).toBeVisible(); + await expect(contentRow.root.getByLabel(status)).toBeVisible(); }); - test(`change content to ${state} by checkbox`, async ({ page }) => { - const contentText = await createRandomContent(page, mode); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); + test(`change content from <${currentStatus}> to <${status}> by checkbox`, async ({ contentsPage, contentPage }) => { + await contentsPage.addContent(); - await contentRow.getByRole('checkbox').click(); - await page.getByRole('button', { name: state }).click(); - await page.getByRole('button', { name: 'Confirm' }).click(); + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); - await expect(contentRow.getByLabel(state)).toBeVisible(); + if (currentStatus === 'Published') { + await contentPage.savePublishAndClose(); + } else { + await contentPage.saveAndClose(); + } + + const contentRow = await contentsPage.getContentRow(contentText); + await contentRow.select(); + await contentsPage.changeSelectedStatus(status); + + await expect(contentRow.root.getByLabel(status)).toBeVisible(); }); - test(`change content to ${state} by detail page`, async ({ page }) => { - const contentText = await createRandomContent(page, mode); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); + test(`change content from <${currentStatus}> to <${status}> by detail page`, async ({ page, contentsPage, contentPage }) => { + await contentsPage.addContent(); + + const contentText = `content-${getRandomId()}`; + await contentPage.enterField(contentText); - await contentRow.click(); - await page.getByRole('button', { name: currentState }).click(); - await page.getByText(`Change to ${state}`).click(); - await page.getByRole('button', { name: 'Confirm' }).click(); + if (currentStatus === 'Published') { + await contentPage.savePublishAndEdit(); + } else { + await contentPage.saveAndEdit(); + } - await expect(page.getByRole('button', { name: state })).toBeVisible(); + const dropdown = await contentPage.openStatusDropdown(currentStatus); + await dropdown.actionConfirm(`Change to ${status}`); + + await expect(page.getByRole('button', { name: status })).toBeVisible(); }); }); -test('delete content', async ({ dropdown, page }) => { - await createRandomContent(page, 'SaveAndEdit'); +test('delete content by details', async ({ page, contentsPage, contentPage }) => { + const contentText = `field-${getRandomId()}`; + + await contentsPage.addContent(); + + await contentPage.enterField(contentText); + await contentPage.saveAndEdit(); - await page.getByLabel('Options').click(); + const dropdown = await contentPage.openOptionsDropdown(); await dropdown.delete(); await expect(page.getByLabel('Identity')).not.toBeVisible(); }); -test('delete content by options', async ({ dropdown, page }) => { - const contentText = await createRandomContent(page, 'SaveAndClose'); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); +test('delete content by options', async ({ contentsPage, contentPage }) => { + const contentText = await createRandomContent(contentsPage, contentPage); + const contentRow = await contentsPage.getContentRow(contentText); - await contentRow.getByLabel('Options').click(); + const dropdown = await contentRow.openOptionsDropdown(); await dropdown.delete(); - await expect(contentRow).not.toBeVisible(); + await expect(contentRow.root).not.toBeVisible(); }); -test('delete content by checkbox', async ({ page }) => { - const contentText = await createRandomContent(page, 'SaveAndClose'); - const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) }); +test('delete content by checkbox', async ({ contentsPage, contentPage }) => { + const contentText = await createRandomContent(contentsPage, contentPage); + const contentRow = await contentsPage.getContentRow(contentText); - await contentRow.getByRole('checkbox').click(); - await page.getByRole('button', { name: 'Delete' }).click(); - await page.getByRole('button', { name: 'Yes' }).click(); + await contentRow.select(); + await contentsPage.deleteSelected(); - await expect(contentRow).not.toBeVisible(); + await expect(contentRow.root).not.toBeVisible(); }); -async function createRandomContent(page: Page, mode: SaveMode) { +async function createRandomContent(contentsPage: ContentsPage, contentPage: ContentPage) { const contentText = `content-${getRandomId()}`; - await page.getByRole('button', { name: /New/ }).click(); + await contentsPage.addContent(); - // Define content value. - await page.locator('sqx-field-editor').getByRole('textbox').fill(contentText); - await saveContent(page, mode); - - // Wait for the success alert. - await page.getByRole('alert').getByText('Content created successfully.').waitFor({ state: 'visible' }); + await contentPage.enterField(contentText); + await contentPage.saveAndClose(); return contentText; } - -type SaveMode = - 'Save' | - 'SaveAndAdd' | - 'SaveAndClose' | - 'SaveAndEdit' | - 'SavePublishAndAdd' | - 'SavePublishAndClose' | - 'SavePublishAndEdit'; - -async function saveContent(page: Page, mode: SaveMode) { - switch (mode) { - case 'SaveAndAdd': - await page.getByLabel('Save', { exact: true }).getByLabel('More').click(); - await page.getByText('Save & add another').click(); - break; - case 'SaveAndClose': - await page.getByLabel('Save', { exact: true }).getByLabel('More').click(); - await page.getByText('Save & close').click(); - break; - case 'SaveAndEdit': - await page.getByRole('button', { name: 'Save', exact: true }).click(); - break; - case 'SavePublishAndAdd': - await page.getByLabel('Save and Publish').getByLabel('More').click(); - await page.getByText('Save and Publish & add another').click(); - break; - case 'SavePublishAndClose': - await page.getByLabel('Save and Publish').getByLabel('More').click(); - await page.getByText('Save and Publish & close').click(); - break; - case 'SavePublishAndEdit': - await page.getByRole('button', { name: 'Save and Publish' }).click(); - break; - case 'Save': - await page.getByRole('button', { name: 'Save', exact: true }).click(); - break; - } -} diff --git a/tools/e2e/tests/start-page.spec.ts b/tools/e2e/tests/login-start.spec.ts similarity index 72% rename from tools/e2e/tests/start-page.spec.ts rename to tools/e2e/tests/login-start.spec.ts index dab028cfc0..3431f7ce61 100644 --- a/tools/e2e/tests/start-page.spec.ts +++ b/tools/e2e/tests/login-start.spec.ts @@ -5,10 +5,10 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { expect, test } from '@playwright/test'; +import { expect, test } from './_fixture'; -test.beforeEach(async ({ page }) => { - await page.goto('/'); +test.beforeEach(async ({ loginPage }) => { + await loginPage.goto(); }); test('has title', async ({ page }) => { diff --git a/tools/e2e/tests/login.spec.ts b/tools/e2e/tests/login.spec.ts index 7c9f28f421..8d27e0f3b6 100644 --- a/tools/e2e/tests/login.spec.ts +++ b/tools/e2e/tests/login.spec.ts @@ -5,38 +5,25 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { expect, test } from '@playwright/test'; +import { expect, test } from './_fixture'; -test.beforeEach(async ({ page }) => { - await page.goto('/'); +test.beforeEach(async ({ loginPage }) => { + await loginPage.goto(); }); -test('login', async ({ page }) => { - // Start waiting for popup before clicking. - const popupPromise = page.waitForEvent('popup'); - - await page.getByTestId('login').click(); - - const popup = await popupPromise; - await popup.waitForLoadState(); - - await popup.getByTestId('login-button').waitFor(); - - await popup.getByPlaceholder('Enter Email').fill('hello@squidex.io'); - await popup.getByPlaceholder('Enter Password').fill('1q2w3e$R'); - await popup.getByTestId('login-button').click(); +test('login', async ({ page, loginPage }) => { + const popup = await loginPage.openPopup(); + await popup.enterEmail('hello@squidex.io'); + await popup.enterPassword('1q2w3e$R'), + await popup.login(); await page.waitForURL(/app/); await expect(page).toHaveTitle(/Apps/); }); -test('visual test', async ({ page }) => { - // Start waiting for popup before clicking. - const popupPromise = page.waitForEvent('popup'); - - await page.getByTestId('login').click(); +test('visual test', async ({ loginPage }) => { + const popup = await loginPage.openPopup(); - const popup = await popupPromise; - await expect(popup).toHaveScreenshot({ fullPage: true }); + await expect(popup.root).toHaveScreenshot({ fullPage: true }); }); \ No newline at end of file diff --git a/tools/e2e/tests/pages/apps.ts b/tools/e2e/tests/pages/apps.ts new file mode 100644 index 0000000000..603f957296 --- /dev/null +++ b/tools/e2e/tests/pages/apps.ts @@ -0,0 +1,38 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Page } from '@playwright/test'; + +export class AppsPage { + constructor(private readonly page: Page) {} + + public async goto() { + await this.page.goto('/app'); + } + + public async gotoApp(name: string) { + await this.page.getByRole('heading', { name }).click(); + } + + public async openAppDialog() { + await this.page.getByTestId('new-app').click(); + + return new AppDialog(this.page); + } +} + +class AppDialog { + constructor(private readonly page: Page) {} + + public async enterName(name: string) { + await this.page.locator('#name').fill(name); + } + + public async save() { + await this.page.getByRole('button', { name: 'Create' }).click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/content.ts b/tools/e2e/tests/pages/content.ts new file mode 100644 index 0000000000..f382c484b8 --- /dev/null +++ b/tools/e2e/tests/pages/content.ts @@ -0,0 +1,76 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Page } from '@playwright/test'; +import { Dropdown } from './dropdown'; + +export class ContentPage { + constructor(private readonly page: Page) {} + + public async back() { + await this.page.getByLabel('Back').click(); + } + + public async enterField(value: string) { + await this.page.locator('sqx-field-editor').getByRole('textbox').fill(value); + } + + public async saveAndAdd() { + await this.page.getByLabel('Save', { exact: true }).getByLabel('More').click(); + await this.page.getByText('Save & add another').click(); + await this.waitForCreation(); + } + + public async saveAndClose() { + await this.page.getByLabel('Save', { exact: true }).getByLabel('More').click(); + await this.page.getByText('Save & close').click(); + await this.waitForCreation(); + } + + public async saveAndEdit() { + await this.page.getByRole('button', { name: 'Save', exact: true }).click(); + await this.waitForCreation(); + } + + public async savePublishAndAdd() { + await this.page.getByLabel('Save and Publish').getByLabel('More').click(); + await this.page.getByText('Save and Publish & add another').click(); + await this.waitForCreation(); + } + + public async savePublishAndClose() { + await this.page.getByLabel('Save and Publish').getByLabel('More').click(); + await this.page.getByText('Save and Publish & close').click(); + await this.waitForCreation(); + } + + public async savePublishAndEdit() { + await this.page.getByRole('button', { name: 'Save and Publish', exact: true }).click(); + await this.waitForCreation(); + } + + public async save() { + await this.page.getByRole('button', { name: 'Save', exact: true }).click(); + await this.waitForCreation(); + } + + public async openStatusDropdown(status: string) { + await this.page.getByRole('button', { name: status }).click(); + + return new Dropdown(this.page); + } + + public async openOptionsDropdown() { + await this.page.getByLabel('Options').click(); + + return new Dropdown(this.page); + } + + private async waitForCreation() { + await this.page.getByRole('alert').getByText('Content created successfully.').waitFor({ state: 'visible' }); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/contents.ts b/tools/e2e/tests/pages/contents.ts new file mode 100644 index 0000000000..cab1c239d8 --- /dev/null +++ b/tools/e2e/tests/pages/contents.ts @@ -0,0 +1,63 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Locator, Page } from '@playwright/test'; +import { escapeRegex } from '../utils'; +import { Dropdown } from './dropdown'; + +export class ContentsPage { + constructor(private readonly page: Page) {} + + public async goto(appName: string, schemaName: string) { + await this.page.goto(`/app/${appName}/content/${schemaName}`); + } + + public async increasePageSize() { + await this.page.getByRole('combobox').selectOption('3: 50'); + } + + public async addContent() { + await this.page.getByRole('button', { name: /New/ }).click(); + } + + public async changeSelectedStatus(status: string) { + await this.page.getByRole('button', { name: status }).click(); + await this.page.getByRole('button', { name: 'Confirm' }).click(); + } + + public async deleteSelected() { + await this.page.getByRole('button', { name: 'Delete' }).click(); + await this.page.getByRole('button', { name: 'Yes' }).click(); + } + + public async getContentRow(text: string) { + const locator = this.page.locator('tr', { hasText: escapeRegex(text) }); + + return new ContentRow(this.page, locator); + } +} + +export class ContentRow { + constructor(private readonly page: Page, + public readonly root: Locator, + ) { + } + + public async edit() { + await this.root.click(); + } + + public async select() { + await this.root.getByRole('checkbox').click(); + } + + public async openOptionsDropdown() { + await this.root.getByLabel('Options').click(); + + return new Dropdown(this.page); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/dropdown.ts b/tools/e2e/tests/pages/dropdown.ts new file mode 100644 index 0000000000..43bfc1875e --- /dev/null +++ b/tools/e2e/tests/pages/dropdown.ts @@ -0,0 +1,27 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Page } from '@playwright/test'; + +export class Dropdown { + constructor(private readonly page: Page) {} + + public async delete() { + await this.page.getByText('Delete').click(); + await this.page.getByRole('button', { name: /Yes/ }).click(); + } + + public async action(name: string) { + await this.page.getByText(name).click(); + await this.page.locator('sqx-dropdown-menu').waitFor({ state: 'hidden' }); + } + + public async actionConfirm(name: string) { + await this.action(name); + await this.page.getByRole('button', { name: 'Confirm' }).click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/index.ts b/tools/e2e/tests/pages/index.ts new file mode 100644 index 0000000000..df07eac06e --- /dev/null +++ b/tools/e2e/tests/pages/index.ts @@ -0,0 +1,16 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export * from './apps'; +export * from './content'; +export * from './contents'; +export * from './login'; +export * from './rule'; +export * from './rule'; +export * from './rules'; +export * from './schema'; +export * from './schemas'; \ No newline at end of file diff --git a/tools/e2e/tests/pages/login.ts b/tools/e2e/tests/pages/login.ts new file mode 100644 index 0000000000..8edc6fd2b2 --- /dev/null +++ b/tools/e2e/tests/pages/login.ts @@ -0,0 +1,47 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Page } from '@playwright/test'; + +export class LoginPage { + constructor(private readonly page: Page) {} + + public async goto() { + await this.page.goto('/'); + } + + public async openPopup() { + const popupPromise = this.page.waitForEvent('popup'); + + await this.page.getByTestId('login').click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + + await popup.getByTestId('login-button').waitFor(); + return new LoginPopup(this.page, popup); + } +} + +export class LoginPopup { + constructor(private readonly page: Page, + public readonly root: Page, + ) { + } + + public async enterEmail(email: string) { + await this.root.getByPlaceholder('Enter Email').fill(email); + } + + public async enterPassword(password: string) { + await this.root.getByPlaceholder('Enter Password').fill(password); + } + + public async login() { + await this.root.getByTestId('login-button').click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/rule.ts b/tools/e2e/tests/pages/rule.ts new file mode 100644 index 0000000000..26a1802722 --- /dev/null +++ b/tools/e2e/tests/pages/rule.ts @@ -0,0 +1,30 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Page } from '@playwright/test'; + +export class RulePage { + constructor(private readonly page: Page) {} + + public async selectContentChangedTrigger() { + await this.page.getByText('Content changed').click(); + } + + public async selectWebhookAction() { + await this.page.getByText('Webhook').click(); + await this.page.locator('sqx-formattable-input').first().getByRole('textbox').fill('https:/squidex.io'); + } + + public async save() { + await this.page.getByRole('button', { name: 'Save' }).click(); + await this.page.getByText('Enabled').waitFor({ state: 'visible' }); + } + + public async back() { + await this.page.getByLabel('Back').click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/rules.ts b/tools/e2e/tests/pages/rules.ts new file mode 100644 index 0000000000..2f9abbf5f1 --- /dev/null +++ b/tools/e2e/tests/pages/rules.ts @@ -0,0 +1,59 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Locator, Page } from '@playwright/test'; +import { escapeRegex } from '../utils'; +import { Dropdown } from './dropdown'; + +export class RulesPage { + constructor(private readonly page: Page) {} + + public async goto(appName: string) { + await this.page.goto(`/app/${appName}/rules`); + } + + public async addRule() { + await this.page.getByRole('link', { name: /New Rule/ }).click(); + } + + public async renameRule(name: RegExp) { + await this.page.locator('div.card', { hasText: name }).getByRole('heading').first().dblclick(); + + return new RenameDialog(this.page); + } + + public async getRule(name: string) { + const locator = this.page.locator('div.card', { hasText: escapeRegex(name) }); + + return new RuleCard(this.page, locator); + } +} + +export class RuleCard { + constructor(private readonly page: Page, + public readonly root: Locator, + ) { + } + + public async openOptionsDropdown() { + await this.root.getByLabel('Options').click(); + + return new Dropdown(this.page); + } +} + +export class RenameDialog { + constructor(private readonly page: Page) {} + + public async enterName(name: string) { + await this.page.locator('form').getByRole('textbox').fill(name); + } + + public async save() { + await this.page.locator('form').getByLabel('Save').click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/schema.ts b/tools/e2e/tests/pages/schema.ts new file mode 100644 index 0000000000..2d13425a90 --- /dev/null +++ b/tools/e2e/tests/pages/schema.ts @@ -0,0 +1,108 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { expect, Locator, Page } from '@playwright/test'; +import { escapeRegex } from '../utils'; +import { Dropdown } from './dropdown'; + +export class SchemaPage { + constructor(private readonly page: Page) {} + + public async goto(appName: string, schemaName: string) { + await this.page.goto(`/app/${appName}/schemas/${schemaName}`); + } + + public async addField() { + await this.page.locator('button').filter({ hasText: /^Add Field$/ }).click(); + + return new FieldDialog(this.page); + } + + public async addNestedField() { + await this.page.locator('button').filter({ hasText: /Add Nested Field/ }).click(); + + return new FieldDialog(this.page); + } + + public async openOptionsDropdown() { + await this.page.getByLabel('Options').click(); + + return new Dropdown(this.page); + } + + public async getFieldRow(fieldName: string) { + const locator = this.page.locator('div.table-items-row-summary', { hasText: escapeRegex(fieldName) }); + + return new FieldRow(this.page, locator); + } + + public async publish() { + const button = this.page.getByRole('button', { name: 'Published', exact: true }); + await button.click(); + + await expect(button).toBeDisabled(); + } + + public async unpublish() { + const button = this.page.getByRole('button', { name: 'Unpublished', exact: true }); + await button.click(); + + await expect(button).toBeDisabled(); + } +} + +export class FieldRow { + constructor(private readonly page: Page, + public readonly root: Locator, + ) { + } + + public async openOptionsDropdown() { + await this.root.getByLabel('Options').click(); + + return new Dropdown(this.page); + } +} + +export class FieldDialog { + constructor(private readonly page: Page) {} + + public async enterName(name: string) { + await this.page.getByPlaceholder('Enter field name').fill(name); + } + + public async enterType(type: string) { + await this.page.getByText(type, { exact: true }).click(); + } + + public async enterLabel(label: string) { + await this.page.getByLabel('Label').fill(label); + } + + public async createAndClose() { + await this.page.getByTestId('dialog').getByRole('button', { name: 'Create' }).click(); + } + + public async createAndAdd() { + await this.page.getByTestId('dialog').getByLabel('Add field').getByLabel('More').click(); + await this.page.getByText('Create & add another').click(); + } + + public async createAndEdit() { + await this.page.getByTestId('dialog').getByLabel('Add field').getByLabel('More').click(); + await this.page.getByText('Create & edit properties').click(); + } + + public async saveAndClose() { + await this.page.getByTestId('dialog').getByRole('button', { name: 'Save and close' }).click(); + } + + public async saveAndAdd() { + await this.page.getByTestId('dialog').getByLabel('Save field').getByLabel('More').click(); + await this.page.getByText('Save and add field').click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/pages/schemas.ts b/tools/e2e/tests/pages/schemas.ts new file mode 100644 index 0000000000..0d9fb334bf --- /dev/null +++ b/tools/e2e/tests/pages/schemas.ts @@ -0,0 +1,48 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Locator, Page } from '@playwright/test'; +import { escapeRegex } from '../utils'; + +export class SchemasPage { + constructor(private readonly page: Page) {} + + public async goto(appName: string) { + await this.page.goto(`/app/${appName}/schemas`); + } + + public async getSchemaLink(schemaName: string) { + const locator = this.page.locator('a.nav-link', { hasText: escapeRegex(schemaName) }); + + return new SchemaLink(this.page, locator); + } + + public async createSchema() { + await this.page.getByLabel('Create Schema').click(); + + return new CreateDialog(this.page); + } +} + +export class SchemaLink { + constructor(private readonly page: Page, + public readonly root: Locator, + ) { + } +} + +export class CreateDialog { + constructor(private readonly page: Page) {} + + public async enterName(name: string) { + await this.page.getByLabel('Name (required)').fill(name); + } + + public async save() { + await this.page.getByRole('button', { name: 'Create', exact: true }).click(); + } +} \ No newline at end of file diff --git a/tools/e2e/tests/utils.ts b/tools/e2e/tests/utils.ts index 36a7f2daaf..7788e58b52 100644 --- a/tools/e2e/tests/utils.ts +++ b/tools/e2e/tests/utils.ts @@ -7,7 +7,6 @@ import fs from 'fs/promises'; import path from 'path'; -import { Page } from '@playwright/test'; import { TEMPORARY_PATH } from '../playwright.config'; let COUNTER = 0; @@ -48,72 +47,4 @@ async function getPath(name: string) { await fs.mkdir(TEMPORARY_PATH, { recursive: true }); return fullPath; -} - -export type FieldSaveMode = - 'CreateAndAdd' | - 'CreateAndClose' | - 'CreateAndEdit' | - 'SaveAndAdd' | - 'SaveAndClose'; - -export async function createSchema(page: Page, args: { name: string; mode?: FieldSaveMode }) { - const { name } = args; - - await page.getByLabel('Create Schema').click(); - - // Define schema name. - await page.getByLabel('Name (required)').fill(name); - - // Save schema. - await page.getByRole('button', { name: 'Create', exact: true }).click(); -} - -export async function createField(page: Page, args: { name: string; type?: string; mode?: FieldSaveMode }) { - const { name, type, mode } = args; - - await page.locator('button').filter({ hasText: /^Add Field$/ }).click(); - - // Define field type and name. - await page.getByText(type || 'String', { exact: true }).click(); - await page.getByPlaceholder('Enter field name').fill(name); - - // Save schema. - await saveField(page, mode || 'CreateAndClose'); -} - -export async function createNestedField(page: Page, args: { name: string; type?: string; mode?: FieldSaveMode }) { - const { name, type, mode } = args; - - await page.locator('button').filter({ hasText: /Add Nested Field/ }).click(); - - // Define field type and name. - await page.getByText(type || 'String', { exact: true }).click(); - await page.getByPlaceholder('Enter field name').fill(name); - - // Save schema. - await saveField(page, mode || 'CreateAndClose'); -} - -export async function saveField(page: Page, mode: FieldSaveMode) { - switch (mode) { - case 'CreateAndClose': - await page.getByTestId('dialog').getByRole('button', { name: 'Create' }).click(); - break; - case 'CreateAndAdd': - await page.getByTestId('dialog').getByLabel('Add field').getByLabel('More').click(); - await page.getByText('Create & add another').click(); - break; - case 'CreateAndEdit': - await page.getByTestId('dialog').getByLabel('Add field').getByLabel('More').click(); - await page.getByText('Create & edit properties').click(); - break; - case 'SaveAndClose': - await page.getByTestId('dialog').getByRole('button', { name: 'Save and close' }).click(); - break; - case 'SaveAndAdd': - await page.getByTestId('dialog').getByLabel('Save field').getByLabel('More').click(); - await page.getByText('Save and add field').click(); - break; - } } \ No newline at end of file