diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000000..5b4d03e4a8 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,42 @@ +name: Run E2E tests + +on: + workflow_call: + +jobs: + e2e-tests: + name: ${{matrix.browser}} (${{ matrix.shard }}/${{ matrix.shards }}) + strategy: + fail-fast: false + matrix: + browser: [chromium, firefox] + shard: [1, 2, 3, 4, 5, 6] + shards: [6] + + runs-on: ubuntu-latest + + steps: + - + uses: actions/checkout@v3 + - + name: Playwright E2E Tests + run: make e2e-tests-ci browser=${{ matrix.browser }} shard="${{ matrix.shard }}/${{ matrix.shards }}" + - + name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results-${{ matrix.browser }}-${{ matrix.shard }} + path: test/e2e/test-results + if-no-files-found: error + + e2e-tests-result: + if: always() + runs-on: ubuntu-latest + needs: e2e-tests + steps: + - name: Check aggregated E2E result + if: ${{ needs.e2e-tests.result != 'success' }} + run: | + echo "Some E2E tests failed" + exit 1 diff --git a/.github/workflows/integrate-and-deploy.yml b/.github/workflows/integrate-and-deploy.yml index bbad146060..57fbba9720 100644 --- a/.github/workflows/integrate-and-deploy.yml +++ b/.github/workflows/integrate-and-deploy.yml @@ -74,17 +74,6 @@ jobs: - name: Unit Tests run: make unit-tests-ci - - - name: Playwright E2E Tests - run: make e2e-tests-ci - - - name: Upload Playwright test results - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-results - path: test/e2e/test-results - if-no-files-found: error - name: Log in to Docker Hub uses: docker/login-action@v2 @@ -109,10 +98,13 @@ jobs: IMAGE_NEXT_APP: ${{ steps.image.outputs.NAMESPACE }}:${{ steps.image.outputs.TAG_NEXT_APP }} IMAGE_LFMERGE: ${{ steps.image.outputs.LFMERGE_NAMESPACE }}:${{ steps.image.outputs.TAG_LFMERGE }} + e2e-tests: + uses: ./.github/workflows/e2e-tests.yml + deploy: runs-on: [self-hosted, languageforge] - needs: integrate + needs: [integrate, e2e-tests] steps: - diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d6f54d67d1..2f5b7feda5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -54,45 +54,8 @@ jobs: github_token: ${{ github.token }} junit_files: PhpUnitTests.xml - build-app-run-e2e-tests: - runs-on: ubuntu-latest - - steps: - - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - with: - node-version: '16.14.0' - cache: 'npm' - - - name: Get installed Playwright version - id: playwright-version - run: echo -n "version=$(npm ls @playwright/test --json | jq --raw-output '.version')" >> $GITHUB_OUTPUT - - - name: Cache playwright browsers - uses: actions/cache@v3 - id: playwright-cache - with: - path: '~/.cache/ms-playwright' - # Make each playwright version use its own cache in case it uses different browser versions - # Cache entries are deleted if not accessed for 7 days - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - key: 'playwright-${{ steps.playwright-version.outputs.version }}' - - - - name: Playwright E2E Tests - # see https://playwright.dev/docs/ci#github-actions - run: make e2e-tests-ci - - - - name: Upload Playwright test results - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-results - path: test/e2e/test-results - if-no-files-found: error + e2e-tests: + uses: ./.github/workflows/e2e-tests.yml check-code-formatting: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 72a18721d2..4b1a8e02f5 100644 --- a/Makefile +++ b/Makefile @@ -16,14 +16,14 @@ ui-builder: e2e-tests-ci: npm ci $(MAKE) e2e-app - npx playwright install chromium - npx playwright test -c ./test/e2e/playwright.config.ts + npx playwright install ${browser} --with-deps + npx playwright test -c ./test/e2e/playwright.config.ts --project=${browser} --shard=${shard} .PHONY: e2e-tests e2e-tests: ui-builder npm install $(MAKE) e2e-app - npx playwright install chromium + npx playwright install chromium firefox npx playwright test -c ./test/e2e/playwright.config.ts $(params) .PHONY: e2e-app diff --git a/test/e2e/fixtures.ts b/test/e2e/fixtures.ts index 5c278abbbc..8068ce7da8 100644 --- a/test/e2e/fixtures.ts +++ b/test/e2e/fixtures.ts @@ -2,6 +2,7 @@ import type { Browser, Page } from '@playwright/test'; import { test as base, APIRequestContext } from '@playwright/test'; import { users } from './constants'; import { getStorageStatePath, Project, ProjectTestService, UserDetails, UserTestService } from './utils'; +import { TestProject } from './utils/types'; export type Tab = Page; export type E2EUsername = keyof typeof users; @@ -75,3 +76,20 @@ export function projectPerTest(lazy?: boolean): () => Project | Promise return () => currentTestProject; } } + +export function defaultProject(): TestProject { + + let project: Project; + let entryIds: string[]; + + test.beforeAll(async ({ projectService }, testInfo) => { + const defaultProject = await projectService.createDefaultProject(testInfo); + project = defaultProject.project(); + entryIds = defaultProject.entryIds(); + }); + + return { + project: () => project, + entryIds: () => entryIds, + }; +} diff --git a/test/e2e/global-setup.ts b/test/e2e/global-setup.ts index 12ce1fd74d..7b985ab0b9 100644 --- a/test/e2e/global-setup.ts +++ b/test/e2e/global-setup.ts @@ -1,4 +1,4 @@ -import { chromium, firefox, FullConfig, Page, webkit } from '@playwright/test'; +import { BrowserType, chromium, firefox, FullConfig, Page, webkit } from '@playwright/test'; import * as fs from 'fs'; import { appUrl, users } from './constants'; import { getStorageStatePath, login, UserDetails, UserTestService } from './utils'; @@ -20,14 +20,29 @@ async function initE2EUser(page: Page, user: UserDetails) { await context.storageState({ path }); } +/** + * @returns The first project browser that is installed (different CI jobs use/install different browsers) + */ +function findInstalledBrowser(config: FullConfig): BrowserType { + const browserTypes = config.projects.map((project) => { + const browserType = { chromium, firefox, webkit }[project.use.defaultBrowserType]; + return { + browserType, + installed: fs.existsSync(browserType.executablePath()), + } + }); + + return browserTypes.find((browser) => browser.installed).browserType; +} + export default async function globalSetup(config: FullConfig) { console.log('Starting global setup\n'); console.time('Global setup took'); try { - const use = config.projects[0].use; - const browserType = { chromium, firefox, webkit }[use.defaultBrowserType]; + const browserType = findInstalledBrowser(config); const browser = await browserType.launch(); + for (const user of Object.values(users)) { const context = await browser.newContext({ baseURL: appUrl }); const page = await context.newPage(); @@ -35,7 +50,7 @@ export default async function globalSetup(config: FullConfig) { await context.close(); } } catch (error) { - console.warn(`Error in Playwright global setup: ${error}.`); + console.error(`Error in Playwright global setup: ${error}.`); throw error; } diff --git a/test/e2e/pages/base-page.ts b/test/e2e/pages/base-page.ts index a934d26653..41c1ac64bd 100644 --- a/test/e2e/pages/base-page.ts +++ b/test/e2e/pages/base-page.ts @@ -67,10 +67,8 @@ export abstract class BasePage * @returns the page for convenience/chaining */ async reload(): Promise { - await Promise.all([ - this.page.reload(), - this.waitFor(), - ]).catch(error => { + await this.page.reload(); + await this.waitFor().catch(error => { console.error(error); throw error; }); diff --git a/test/e2e/pages/configuration-fields.tab.ts b/test/e2e/pages/configuration-fields.tab.ts index fd71bab857..729c2ef44e 100644 --- a/test/e2e/pages/configuration-fields.tab.ts +++ b/test/e2e/pages/configuration-fields.tab.ts @@ -17,7 +17,7 @@ export class ConfigurationPageFieldsTab extends ConfigurationPage { return row.locator('css=td,th').nth(columnIndex).locator('input'); } - async toggleField(tableTitle: string, field: string): Promise { + async toggleFieldExpanded(tableTitle: string, field: string): Promise { const row: Locator = this.getRow(this.getTable(tableTitle), field); await row.locator('.field-specific-btn').click(); } diff --git a/test/e2e/pages/editor.page.ts b/test/e2e/pages/editor.page.ts index 99cf838353..ec780eaf6b 100644 --- a/test/e2e/pages/editor.page.ts +++ b/test/e2e/pages/editor.page.ts @@ -28,11 +28,6 @@ export class EditorPage extends BasePage { }; readonly renderedDivs = this.locator('.dc-rendered-entryContainer'); - readonly search = { - searchInput: this.locator('#editor-entry-search-entries'), - matchCount: this.locator('#totalNumberOfEntries >> span') - }; - readonly noEntries = this.locator('.no-entries'); readonly entryCard = this.locator('.entry-card'); readonly senseCard = this.locator('.dc-sense.card'); @@ -178,20 +173,16 @@ export class EditorPage extends BasePage { return card.locator('[data-ng-switch-when="pictures"]'); } - // returns the locator to the picture or undefined if 0 or more than one pictures with this filename are found - async getPicture(card: Locator, filename: string): Promise { - const picture: Locator = card.locator(`img[src$="${filename}"]`); - return (await picture.count() == 1 ? picture : undefined); + picture(filename: string, parent = this.senseCard): Locator { + return parent.locator(`img[src$="${filename}"]`); } - async getPictureDeleteButton(card: Locator, pictureFilename: string): Promise { - const button = card.locator(`[data-ng-repeat="picture in $ctrl.pictures"]:has(img[src$="${pictureFilename}"]) >> [title="Delete Picture"]`); - return (await button.count() == 1 ? button : undefined); + deletePictureButton(pictureFilename: string, parent = this.senseCard): Locator { + return parent.locator(`[data-ng-repeat="picture in $ctrl.pictures"]:has(img[src$="${pictureFilename}"]) >> [title="Delete Picture"]`); } - async getPictureCaption(picture: Locator, languageCode: string = "en"): Promise { - const caption = picture.locator(`xpath=..//.. >> div.input-group:has-Text("${languageCode}") >> textarea`); - return (await caption.count() == 1 ? caption : undefined); + caption(picture: Locator, languageCode: string = "en"): Locator { + return picture.locator(`xpath=..//.. >> div.input-group:has-Text("${languageCode}") >> textarea`); } getCancelDropboxButton(card: Locator, uploadType: UploadType): Locator { diff --git a/test/e2e/pages/entry-list.page.ts b/test/e2e/pages/entry-list.page.ts index 643dd829d1..c03856098e 100644 --- a/test/e2e/pages/entry-list.page.ts +++ b/test/e2e/pages/entry-list.page.ts @@ -3,11 +3,13 @@ import { Project } from '../utils'; import { BasePage } from './base-page'; export class EntryListPage extends BasePage { - readonly totalNumberOfEntries = this.locator('#totalNumberOfEntries'); + readonly entries = this.locator('.lexiconItemListContainer').locator('.lexiconListItem, .lexiconListItemCompact'); readonly filterInput = this.locator('[placeholder="Search"]'); readonly filterInputClearButton = this.locator('.clear-search-button'); readonly matchCount = this.locator('#totalNumberOfEntries >> span'); - readonly createNewWordButton = this.locator('#newWord:visible, #noEntriesNewWord:visible'); + readonly createNewWordButton = this.locator('#newWord:visible, #noEntriesNewWord:visible, #editorNewWordBtn:visible'); + + private readonly totalNumberOfEntries = this.locator('#totalNumberOfEntries'); entry(lexeme: string): Locator { return this.locator(`.lexiconListItem:visible:has(span:has-text("${lexeme}"))`); diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index fb6a368c6d..e374459f6f 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -34,8 +34,9 @@ const config: PlaywrightTestConfig = { globalSetup: require.resolve('./global-setup'), /* Retry on CI only */ retries: process.env.CI ? 1 : 0, - /* Opt out of parallel tests. Our current state management prevents a user from working on different projects simultaneously. */ - workers: process.env.CI ? 1 : 1, + /* Our current state management prevents an LF-user from working on different projects simultaneously. + Instead, we use Playwright's sharding feature to parallelize tests accross multiple environments. */ + workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ outputDir: 'test-results', // referenced in pull-request.yml reporter: process.env.CI @@ -70,12 +71,12 @@ const config: PlaywrightTestConfig = { }, }, - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, // { // name: 'webkit', diff --git a/test/e2e/tests/__expected-screenshots__/editor-entry.spec.ts/Entry-Editor-and-Entries-List-Entry-Editor-Picture-First-picture-and-caption-is-present-1-chromium.png b/test/e2e/tests/__expected-screenshots__/editor/editor.pictures.spec.ts/Editor-pictures-First-picture-and-caption-is-present-1-chromium.png similarity index 100% rename from test/e2e/tests/__expected-screenshots__/editor-entry.spec.ts/Entry-Editor-and-Entries-List-Entry-Editor-Picture-First-picture-and-caption-is-present-1-chromium.png rename to test/e2e/tests/__expected-screenshots__/editor/editor.pictures.spec.ts/Editor-pictures-First-picture-and-caption-is-present-1-chromium.png diff --git a/test/e2e/tests/__expected-screenshots__/editor-entry.spec.ts/Entry-Editor-and-Entries-List-Entry-Editor-Picture-First-picture-and-caption-is-present-1-firefox.png b/test/e2e/tests/__expected-screenshots__/editor/editor.pictures.spec.ts/Editor-pictures-First-picture-and-caption-is-present-1-firefox.png similarity index 100% rename from test/e2e/tests/__expected-screenshots__/editor-entry.spec.ts/Entry-Editor-and-Entries-List-Entry-Editor-Picture-First-picture-and-caption-is-present-1-firefox.png rename to test/e2e/tests/__expected-screenshots__/editor/editor.pictures.spec.ts/Editor-pictures-First-picture-and-caption-is-present-1-firefox.png diff --git a/test/e2e/tests/editor-entry.spec.ts b/test/e2e/tests/editor-entry.spec.ts deleted file mode 100644 index c6ab5cad4b..0000000000 --- a/test/e2e/tests/editor-entry.spec.ts +++ /dev/null @@ -1,720 +0,0 @@ -import { expect, Locator } from '@playwright/test'; -import { entries, users } from '../constants'; -import { projectPerTest, test } from '../fixtures'; -import { ConfigurationPageFieldsTab, EditorPage, EntryListPage } from '../pages'; -import { ConfirmModal } from '../pages/components'; -import { Project, testFilePath } from '../utils'; - -test.describe('Entry Editor and Entries List', () => { - - let project: Project; - let lexEntriesIds: string[] = []; - - test.beforeAll(async ({ projectService }, testInfo) => { - project = await projectService.initTestProject(testInfo.titlePath[0], undefined, users.manager, [users.member]); - await projectService.addUserToProject(project, users.observer, "observer"); - await projectService.addWritingSystemToProject(project, 'th-fonipa', 'tipa'); - await projectService.addWritingSystemToProject(project, 'th-Zxxx-x-audio', 'taud'); - - await projectService.addPictureFileToProject(project, entries.entry1.senses[0].pictures[0].fileName); - await projectService.addAudioVisualFileToProject(project, entries.entry1.lexeme['th-Zxxx-x-audio'].value); - // put in data - lexEntriesIds.push(await projectService.addLexEntry(project.code, entries.entry1)); - lexEntriesIds.push(await projectService.addLexEntry(project.code, entries.entry2)); - lexEntriesIds.push(await projectService.addLexEntry(project.code, entries.multipleMeaningEntry)); - }); - - test.describe('Entries List', () => { - - let entryListPageManager: EntryListPage; - - test.beforeEach(async ({ managerTab }) => { - entryListPageManager = await new EntryListPage(managerTab, project).goto(); - }); - - test('Entries list has correct number of entries', async () => { - await entryListPageManager.expectTotalNumberOfEntries(lexEntriesIds.length); - }); - - test('Search function works correctly', async () => { - await entryListPageManager.filterInput.fill('asparagus'); - await expect(entryListPageManager.matchCount).toContainText(/1(?= \/)/); - - // remove filter, filter again, have same result - await entryListPageManager.filterInputClearButton.click(); - await entryListPageManager.filterInput.fill('asparagus'); - await expect(entryListPageManager.matchCount).toContainText(/1(?= \/)/); - // remove filter for next test - if this tests fails, the afterEach ensure that it does not impact the next test - await entryListPageManager.filterInputClearButton.click(); - }); - - test('Can click on first entry', async () => { - const [, editorPageManager] = await Promise.all([ - entryListPageManager.entry(entries.entry1.lexeme.th.value).click(), - new EditorPage(entryListPageManager.page, project).waitFor(), - ]) - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toHaveValue(entries.entry1.lexeme.th.value); - }); - - }); - - test.describe('Entry Editor', () => { - - test.describe('Entries', () => { - - let editorPageManager: EditorPage; - - test.beforeEach(({ managerTab }) => { - editorPageManager = new EditorPage(managerTab, project); - }); - - test('Can go from entry editor to entries list', async () => { - await editorPageManager.goto(); - await Promise.all([ - editorPageManager.navigateToEntriesList(), - new EntryListPage(editorPageManager.page, project).waitFor(), - ]); - }); - - // left side bar entries list - test('Editor page has correct entry count in left side bar entries list', async () => { - await editorPageManager.goto(); - await expect(editorPageManager.compactEntryListItem).toHaveCount(lexEntriesIds.length); - }); - - test('Entry 1: edit page has correct definition, part of speech', async () => { - await editorPageManager.goto(); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard, 'Definition', 'en')) - .toHaveValue(entries.entry1.senses[0].definition.en.value); - expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard, 'Part of Speech')) - .toEqual(entries.entry1.senses[0].partOfSpeech.displayName); - }); - - test('Add citation form as visible field', async ({ managerTab }) => { - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project).goto(); - await configurationPage.tabLinks.fields.click(); - await (await configurationPage.getCheckbox('Entry Fields', 'Citation Form', 'Hidden if Empty')).uncheck(); - await configurationPage.applyButton.click(); - await editorPageManager.goto(); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Citation Form', 'th')).toBeVisible(); - }); - - test('Citation form field overrides lexeme form in dictionary citation view', async ({ managerTab }) => { - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project).goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (IPA)')).check(); - await configurationPage.applyButton.click(); - - await editorPageManager.goto(); - - // Dictionary citation reflects lexeme form when citation form is empty - await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme.th.value, entries.entry1.lexeme.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme['th-fonipa'].value, entries.entry1.lexeme['th-fonipa'].value]); - await expect(editorPageManager.renderedDivs).not.toContainText(['citation form', 'citation form']); - await editorPageManager.showExtraFields(); - const citationFormInput = editorPageManager.getTextarea(editorPageManager.entryCard, 'Citation Form', 'th'); - await citationFormInput.fill('citation form'); - - await expect(editorPageManager.renderedDivs).toContainText(['citation form', 'citation form']); - await expect(editorPageManager.renderedDivs).not.toContainText([entries.entry1.lexeme.th.value, entries.entry1.lexeme.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme['th-fonipa'].value, entries.entry1.lexeme['th-fonipa'].value]); - - await citationFormInput.fill(''); - await expect(editorPageManager.renderedDivs).not.toContainText(['citation form', 'citation form']); - await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme.th.value, entries.entry1.lexeme.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme['th-fonipa'].value, entries.entry1.lexeme['th-fonipa'].value]); - }); - }); - - test.describe('Picture', () => { - - const newProject = projectPerTest(true); - - let editorPageManager: EditorPage; - - test.beforeEach(({ managerTab }) => { - editorPageManager = new EditorPage(managerTab, project); - }); - - test('First picture and caption is present', async ({ projectService }) => { - const screenshotProject: Project = await newProject(); - await projectService.addLexEntry(screenshotProject.code, entries.entry1); - await projectService.addPictureFileToProject(screenshotProject, entries.entry1.senses[0].pictures[0].fileName); - - const editorPagePicture = await new EditorPage(editorPageManager.page, screenshotProject).goto(); - const picture: Locator = await editorPagePicture.getPicture(editorPagePicture.senseCard, entries.entry1.senses[0].pictures[0].fileName); - const img = await picture.elementHandle(); - await expect(editorPagePicture.page).toHaveScreenshot({ clip: await img.boundingBox() }); - const caption = await editorPagePicture.getPictureCaption(picture); - await expect(caption).toHaveValue(entries.entry1.senses[0].pictures[0].caption.en.value); - }); - - test('File upload drop box is displayed when Add Picture is clicked and can be cancelled', async () => { - await editorPageManager.goto(); - const addPictureButton = editorPageManager.senseCard.locator(editorPageManager.addPictureButtonSelector); - await expect(addPictureButton).toBeVisible(); - const dropbox = editorPageManager.senseCard.locator(editorPageManager.dropbox.dragoverFieldSelector); - await expect(dropbox).not.toBeVisible(); - const cancelAddingPicture = editorPageManager.getCancelDropboxButton(editorPageManager.senseCard, 'Picture'); - await expect(cancelAddingPicture).not.toBeVisible(); - - await addPictureButton.click(); - await expect(addPictureButton).not.toBeVisible(); - await expect(dropbox).toBeVisible(); - await expect(cancelAddingPicture).toBeVisible(); - - // file upload drop box is not displayed when Cancel Adding Picture is clicked - await cancelAddingPicture.click(); - await expect(addPictureButton).toBeVisible(); - await expect(dropbox).not.toBeVisible(); - await expect(cancelAddingPicture).not.toBeVisible(); - }); - - test('Can change config to show pictures and hide empty captions & can change config to show empty captions', async ({ managerTab }) => { - // can change config to show pictures and hide empty captions - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project).goto(); - await configurationPage.tabLinks.fields.click(); - - await (await configurationPage.getCheckbox('Meaning Fields', 'Pictures', 'Hidden if Empty')).uncheck(); - await configurationPage.toggleField('Meaning Fields', 'Pictures'); - await (await configurationPage.getFieldCheckbox('Meaning Fields', 'Pictures', 'Hide Caption If Empty')).check(); - await configurationPage.applyButton.click(); - - await editorPageManager.goto(); - await editorPageManager.showExtraFields(false); - const picture: Locator = await editorPageManager.getPicture(editorPageManager.senseCard, entries.entry1.senses[0].pictures[0].fileName); - expect(picture).not.toBeUndefined(); - const caption = await editorPageManager.getPictureCaption(picture); - await expect(caption).toBeVisible(); - await caption.fill(''); - - // can change config to show empty captions - await configurationPage.goto(); - await configurationPage.tabLinks.fields.click(); - await (await configurationPage.getCheckbox('Meaning Fields', 'Pictures', 'Hidden if Empty')).uncheck(); - await configurationPage.toggleField('Meaning Fields', 'Pictures'); - await (await configurationPage.getFieldCheckbox('Meaning Fields', 'Pictures', 'Hide Caption If Empty')).uncheck(); - await configurationPage.applyButton.click(); - - await editorPageManager.goto(); - await expect(caption).toBeVisible(); - }); - - test('Picture is removed when Delete is clicked & can change config to hide pictures and hide captions', async ({ projectService }) => { - const testProject: Project = await newProject(); - await projectService.addLexEntry(testProject, entries.entry1); - await projectService.addPictureFileToProject(testProject, entries.entry1.senses[0].pictures[0].fileName); - const editorPagePicture = await new EditorPage(editorPageManager.page, testProject).goto(); - - // Picture is removed when Delete is clicked - let picture: Locator = await editorPagePicture.getPicture(editorPagePicture.senseCard, entries.entry1.senses[0].pictures[0].fileName); - expect(picture).not.toBeUndefined(); - await (await editorPagePicture.getPictureDeleteButton(editorPagePicture.senseCard, entries.entry1.senses[0].pictures[0].fileName)).click(); - const confirmModal = new ConfirmModal(editorPagePicture.page); - await confirmModal.confirmButton.click(); - picture = await editorPagePicture.getPicture(editorPagePicture.senseCard, entries.entry1.senses[0].pictures[0].fileName); - expect(picture).toBeUndefined(); - - const configurationPage = await new ConfigurationPageFieldsTab(editorPageManager.page, testProject).goto(); - await configurationPage.tabLinks.fields.click(); - await (await configurationPage.getCheckbox('Meaning Fields', 'Pictures', 'Hidden if Empty')).check(); - await configurationPage.toggleField('Meaning Fields', 'Pictures'); - await (await configurationPage.getFieldCheckbox('Meaning Fields', 'Pictures', 'Hide Caption If Empty')).uncheck(); - await configurationPage.applyButton.click(); - - // can change config to hide pictures and hide captions - await editorPagePicture.goto(); - picture = await editorPagePicture.getPicture(editorPagePicture.senseCard, entries.entry1.senses[0].pictures[0].fileName); - expect(picture).toBeUndefined(); - expect(editorPagePicture.getPicturesOuterDiv(editorPagePicture.senseCard)).not.toBeVisible(); - await editorPagePicture.showExtraFields(); - expect(editorPagePicture.getPicturesOuterDiv(editorPagePicture.senseCard)).toBeVisible(); - await editorPagePicture.showExtraFields(false); - expect(editorPagePicture.getPicturesOuterDiv(editorPagePicture.senseCard)).not.toBeVisible(); - picture = await editorPagePicture.getPicture(editorPagePicture.senseCard, entries.entry1.senses[0].pictures[0].fileName); - expect(picture).toBeUndefined(); - }); - }); - - test.describe('Audio', () => { - test.beforeAll(async ({ managerTab }) => { - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project).goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (IPA)')).check(); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (Voice)')).check(); - await configurationPage.applyButton.click(); - }); - - - test.describe('Member', () => { - let editorPageMember: EditorPage; - - test.beforeEach(async ({ memberTab }) => { - editorPageMember = new EditorPage(memberTab, project); - }); - - test('Audio input system is present, playable and has "more" control (member)', async () => { - await editorPageMember.goto(); - const audio = editorPageMember.getAudioPlayer('Word', 'taud'); - await expect(audio.playIcon).toBeVisible(); - await expect(audio.togglePlaybackAnchor).toBeEnabled(); - - await expect(audio.dropdownToggle).toBeVisible(); - await expect(audio.dropdownToggle).toBeEnabled(); - await expect(audio.uploadButton).not.toBeVisible(); - // this button is only visible when user is observer and has only the right to download - await expect(audio.downloadButton).not.toBeVisible(); - }); - - test('Word 2 (without audio): audio input system is not playable but has "upload" button (member)', async () => { - await editorPageMember.goto({ entryId: lexEntriesIds[1] }); - const audio = editorPageMember.getAudioPlayer('Word', 'taud'); - await expect(audio.togglePlaybackAnchor).not.toBeVisible(); - await expect(audio.dropdownToggle).toBeEnabled(); - await expect(audio.uploadButton).toBeVisible(); - await expect(audio.uploadButton).toBeEnabled(); - await expect(audio.downloadButton).not.toBeVisible(); - }); - }); - - test.describe('Observer', () => { - let editorPageObserver: EditorPage; - - test.beforeEach(async ({ observerTab }) => { - editorPageObserver = new EditorPage(observerTab, project); - }); - - test('Audio Input System is playable but does not have "more" control (observer)', async () => { - await editorPageObserver.goto(); - const audio = editorPageObserver.getAudioPlayer('Word', 'taud'); - await expect(audio.playIcon).toBeVisible(); - await expect(audio.togglePlaybackAnchor).toBeVisible(); - await expect(audio.togglePlaybackAnchor).toBeEnabled(); - await expect(audio.dropdownToggle).not.toBeVisible(); - await expect(audio.uploadButton).not.toBeVisible(); - await expect(audio.downloadButton).toBeVisible(); - }); - - test('Word 2 (without audio): audio input system is not playable and does not have "upload" button (observer)', async () => { - await editorPageObserver.goto({ entryId: lexEntriesIds[1] }); - const audio = editorPageObserver.getAudioPlayer('Word', 'taud'); - await expect(audio.togglePlaybackAnchor).not.toBeVisible(); - await expect(audio.dropdownToggle).not.toBeVisible(); - await expect(audio.uploadButton).not.toBeVisible(); - await expect(audio.downloadButton).not.toBeVisible(); - }); - }); - - test.describe('Manager', () => { - - let editorPageManager: EditorPage; - - test.beforeEach(async ({ managerTab }) => { - editorPageManager = new EditorPage(managerTab, project); - }); - - test('Audio input system is present, playable and has "more" control (manager)', async () => { - await editorPageManager.goto(); - const audio = editorPageManager.getAudioPlayer('Word', 'taud'); - await expect(audio.playIcon).toBeVisible(); - await expect(audio.togglePlaybackAnchor).toBeEnabled(); - await expect(audio.dropdownToggle).toBeVisible(); - await expect(audio.dropdownToggle).toBeEnabled(); - await expect(audio.uploadButton).not.toBeVisible(); - // this button is only visible when user is observer and has only the right to download - await expect(audio.downloadButton).not.toBeVisible(); - }); - - test('Slider is present and updates with seeking', async () => { - await editorPageManager.goto(); - const audio = editorPageManager.getAudioPlayer('Word', 'taud'); - await expect(audio.slider).toBeVisible(); - const bounds = await audio.slider.boundingBox(); - const yMiddle = bounds.y + bounds.height / 2; - await editorPageManager.page.mouse.click(bounds.x + 200, yMiddle); - await expect(audio.audioProgressTime).toContainText("0:01 / 0:02"); - }); - - test('File upload drop box is displayed when Upload is clicked & not displayed if upload cancelled (manager)', async () => { - await editorPageManager.goto(); - const dropbox = editorPageManager.entryCard.locator(editorPageManager.dropbox.dragoverFieldSelector); - await expect(dropbox).not.toBeVisible(); - - const cancelAddingAudio = editorPageManager.getCancelDropboxButton(editorPageManager.entryCard, 'Audio'); - await expect(cancelAddingAudio).not.toBeVisible(); - - const audio = editorPageManager.getAudioPlayer('Word', 'taud'); - await audio.dropdownToggle.click(); - await audio.dropdownMenu.uploadReplacementButton.click(); - await expect(audio.dropdownToggle).not.toBeVisible(); - await expect(dropbox).toBeVisible(); - - await expect(cancelAddingAudio).toBeVisible(); - await cancelAddingAudio.click(); - await expect(audio.dropdownToggle).toBeVisible(); - await expect(dropbox).not.toBeVisible(); - await expect(cancelAddingAudio).not.toBeVisible(); - }); - - test('Navigate to other entries with left entry bar', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[1] }); - - await Promise.all([ - editorPageManager.page.locator('text=' + entries.multipleMeaningEntry.senses[0].definition.en.value).click(), - editorPageManager.page.waitForURL(editorPageManager.entryUrl(lexEntriesIds[2])), - ]); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first(), 'Definition', 'en')).toHaveValue(entries.multipleMeaningEntry.senses[0].definition.en.value); - }); - - test('Word 2 (without audio): audio input system is not playable but has "upload" button (manager)', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[1] }); - const audio = editorPageManager.getAudioPlayer('Word', 'taud'); - await expect(audio.playIcon).not.toBeVisible(); - - await expect(audio.dropdownToggle).not.toBeVisible(); - await expect(audio.uploadButton).toBeVisible(); - await expect(audio.uploadButton).toBeEnabled(); - await expect(audio.downloadButton).not.toBeVisible(); - }); - - test('Can delete audio input system (manager)', async () => { - await editorPageManager.goto(); - const audio = editorPageManager.getAudioPlayer('Word', 'taud'); - await audio.dropdownToggle.click(); - await audio.dropdownMenu.deleteAudioButton.click(); - const confirmModal = new ConfirmModal(editorPageManager.page); - await confirmModal.confirmButton.click(); - await expect(audio.uploadButton).toBeVisible(); - }); - - test('Can\'t upload a non-audio file & can upload audio file', async () => { - // to be independent from the audio deletion test above, go to entry 2 (has no audio) - await editorPageManager.goto({ entryId: lexEntriesIds[1] }); - const noticeElement = editorPageManager.noticeList; - await expect(noticeElement.notices).toHaveCount(0); - - // Can't upload a non-audio file - const audio = editorPageManager.getAudioPlayer('Word', 'taud'); - await audio.uploadButton.click(); - - // Note that Promise.all prevents a race condition between clicking and waiting for the file chooser. - const [fileChooser] = await Promise.all([ - // It is important to call waitForEvent before click to set up waiting. - editorPageManager.page.waitForEvent('filechooser'), - audio.browseButton.click(), - ]); - await fileChooser.setFiles(testFilePath('TestImage.png')); - - await expect(noticeElement.notices).toBeVisible(); - await expect(noticeElement.notices).toContainText(`TestImage.png is not an allowed audio file. Ensure the file is`); - const dropbox = editorPageManager.entryCard.locator(editorPageManager.dropbox.dragoverFieldSelector); - await expect(dropbox).toBeVisible(); - await noticeElement.closeButton.click(); - await expect(noticeElement.notices).toHaveCount(0); - - // Can upload audio file - const [fileChooser2] = await Promise.all([ - editorPageManager.page.waitForEvent('filechooser'), - audio.browseButton.click(), - ]); - await fileChooser2.setFiles(testFilePath('TestAudio.mp3')); - await expect(noticeElement.notices).toHaveCount(1); - await expect(noticeElement.notices).toBeVisible(); - await expect(noticeElement.notices).toContainText('File uploaded successfully'); - await expect(audio.playIcon).toBeVisible(); - await expect(audio.togglePlaybackAnchor).toBeEnabled(); - await expect(audio.dropdownToggle).toBeVisible(); - }); - - test('Word 2: edit page has correct definition, part of speech', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[1] }); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard, 'Definition', 'en')) - .toHaveValue(entries.entry2.senses[0].definition.en.value); - - expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard, 'Part of Speech')) - .toEqual(entries.entry2.senses[0].partOfSpeech.displayName); - }); - - test('Dictionary citation reflects example sentences and translations', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[2] }); - - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[0].sentence.th.value, entries.multipleMeaningEntry.senses[0].examples[0].sentence.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[0].translation.en.value, entries.multipleMeaningEntry.senses[0].examples[0].translation.en.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[1].sentence.th.value, entries.multipleMeaningEntry.senses[0].examples[1].sentence.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[1].translation.en.value, entries.multipleMeaningEntry.senses[0].examples[1].translation.en.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[0].sentence.th.value, entries.multipleMeaningEntry.senses[1].examples[0].sentence.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[0].translation.en.value, entries.multipleMeaningEntry.senses[1].examples[0].translation.en.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[1].sentence.th.value, entries.multipleMeaningEntry.senses[1].examples[1].sentence.th.value]); - await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[1].translation.en.value, entries.multipleMeaningEntry.senses[1].examples[1].translation.en.value]); - }); - - test('Word with multiple definitions: edit page has correct definitions, parts of speech', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[2] }); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first(), 'Definition', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].definition.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1), 'Definition', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].definition.en.value); - - expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard.nth(0), 'Part of Speech')) - .toEqual(entries.multipleMeaningEntry.senses[0].partOfSpeech.displayName); - expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard.nth(1), 'Part of Speech')) - .toEqual(entries.multipleMeaningEntry.senses[1].partOfSpeech.displayName); - }); - - test('Word with multiple meanings: edit page has correct example sentences, translations', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[2] }); - - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Sentence', 'th')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[0].sentence.th.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Translation', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[0].translation.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Sentence', 'th')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[1].sentence.th.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Translation', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[1].translation.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Sentence', 'th')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[0].sentence.th.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Translation', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[0].translation.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Sentence', 'th')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[1].sentence.th.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Translation', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[1].translation.en.value); - }); - - test('While Show Hidden Fields has not been clicked, hidden fields are hidden if they are empty', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[2] }); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(0), 'Semantics Note', 'en')).toHaveCount(0); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(0), 'General Note', 'en')).toBeVisible(); - await editorPageManager.showExtraFields(); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(0), 'Semantics Note', 'en')).toBeVisible(); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(0), 'General Note', 'en')).toBeVisible(); - }); - - test('Word with multiple meanings: edit page has correct general notes, sources', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[2] }); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(0), 'General Note', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].generalNote.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1), 'General Note', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].generalNote.en.value); - await editorPageManager.showExtraFields(); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(0), 'Source', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].source.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1), 'Source', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].source.en.value); - }); - - test('Senses can be reordered and deleted', async () => { - await editorPageManager.goto({ entryId: lexEntriesIds[2] }); - await editorPageManager.senseCard.first().locator(editorPageManager.moveDownButtonSelector).first().click(); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.first(), 'Definition', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[1].definition.en.value); - await expect(editorPageManager.getTextarea( - editorPageManager.senseCard.nth(1), 'Definition', 'en')) - .toHaveValue(entries.multipleMeaningEntry.senses[0].definition.en.value); - }); - - test('Back to browse page, create new word, check word count, modify new word, autosaves changes, new word visible in editor and list', async () => { - const entryListPage = await new EntryListPage(editorPageManager.page, project).goto(); - await entryListPage.createNewWordButton.click(); - // clicking on new word button automatically takes user to entry editor - const entryCount = lexEntriesIds.length + 1; - - await expect(editorPageManager.compactEntryListItem).toHaveCount(entryCount); - - await entryListPage.goto(); - await entryListPage.expectTotalNumberOfEntries(entryCount); - - // go back to editor - await editorPageManager.page.goBack(); - await (editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')) - .fill(entries.entry3.lexeme.th.value); - await (editorPageManager.getTextarea(editorPageManager.senseCard, 'Definition', 'en')) - .fill(entries.entry3.senses[0].definition.en.value); - - const partOfSpeedDropdown = editorPageManager.getDropdown(editorPageManager.senseCard, 'Part of Speech'); - partOfSpeedDropdown.selectOption({ label: 'Noun (n)' }); - - // Autosaves changes - await editorPageManager.page.waitForURL(url => !url.hash.includes('editor/entry/_new_')); - await editorPageManager.page.reload(); - - await expect(partOfSpeedDropdown).toHaveSelectedOption({ label: 'Noun (n)' }); - - const alreadyThere: string = await editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th').inputValue(); - await (editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')) - .fill(alreadyThere + 'a'); - await editorPageManager.page.reload(); - await expect((editorPageManager.getTextarea( - editorPageManager.entryCard, 'Word', 'th'))) - .toHaveValue(entries.entry3.lexeme.th.value + 'a'); - await (editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')) - .fill(entries.entry3.lexeme.th.value); - - // New word is visible in edit page - await editorPageManager.search.searchInput.fill(entries.entry3.senses[0].definition.en.value); - await expect(editorPageManager.search.matchCount).toContainText(/1(?= \/)/); - - // new word is visible in list page - await entryListPage.goto(); - await entryListPage.filterInput.fill(entries.entry3.senses[0].definition.en.value); - await expect(entryListPage.matchCount).toContainText(/1(?= \/)/); - await entryListPage.filterInputClearButton.click(); - - // word count is still correct in browse page - await entryListPage.expectTotalNumberOfEntries(entryCount); - - // remove new word to restore original word count - await entryListPage.entry(entries.entry3.lexeme.th.value).click(); - await editorPageManager.entryCard.first().locator(editorPageManager.deleteCardButtonSelector).first().click(); - - const confirmModal = new ConfirmModal(editorPageManager.page); - await confirmModal.confirmButton.click(); - - await expect(editorPageManager.compactEntryListItem).toHaveCount(lexEntriesIds.length); - - // previous entry is selected after delete - await expect(editorPageManager.getTextarea( - editorPageManager.entryCard, 'Word', 'th')) - .toHaveValue(entries.entry1.lexeme.th.value); - }); - }); - - }); - - test.describe('Entry ID in URL', () => { - test('URL entry id matches selected entry', async ({ managerTab }) => { - const editorPageManager = await new EditorPage(managerTab, project).goto({ entryId: lexEntriesIds[1] }); - expect(editorPageManager.page.url()).toContain(lexEntriesIds[1]); - expect(editorPageManager.page.url()).not.toContain(lexEntriesIds[0]); - - await editorPageManager.goto({ entryId: lexEntriesIds[0] }); - expect(editorPageManager.page.url()).toContain(lexEntriesIds[0]); - expect(editorPageManager.page.url()).not.toContain(lexEntriesIds[1]); - - await editorPageManager.goto({ entryId: lexEntriesIds[1] }); - expect(editorPageManager.page.url()).toContain(lexEntriesIds[1]); - expect(editorPageManager.page.url()).not.toContain(lexEntriesIds[0]); - }); - }); - - test.describe('Configuration check', async () => { - - test.beforeAll(async ({ managerTab }) => { - const configurationPage = new ConfigurationPageFieldsTab(managerTab, project); - await configurationPage.goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (IPA)')).check(); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (Voice)')).check(); - await configurationPage.applyButton.click(); - }); - - - let editorPageManager: EditorPage; - - test.beforeEach(async ({ managerTab }) => { - editorPageManager = new EditorPage(managerTab, project); - }); - - test('Can change configuration to make a writing system visible or invisible', async ({ managerTab }) => { - await editorPageManager.goto(); - // word has only "th", "tipa" and "taud" visible - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toBeVisible(); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'tipa')).toBeVisible(); - await expect(editorPageManager.audioPlayer('Word', 'taud')).toBeVisible(); - - // make "en" input system visible for "Word" field - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project).goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'English')).check(); - await configurationPage.applyButton.click(); - - // check if "en" is visible - await editorPageManager.goto(); - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(4); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'en')).toBeVisible(); - - // make "en" input system invisible for "Word" field - await configurationPage.goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'English')).uncheck(); - await configurationPage.applyButton.click(); - - // check if "en" is invisible - await editorPageManager.goto(); - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'en')).not.toBeVisible(); - }); - - test('Make "taud" input system invisible for "Word" field and "tipa" invisible for manager role, then ensure it worked and change it back', async ({ managerTab, memberTab }) => { - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project).goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - // Make "taud" input system invisible for "Word" field.... - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', '(Voice)')).uncheck(); - // ....and "tipa" invisible for manager role - await (await configurationPage.getCheckbox('Input Systems', 'IPA', 'Manager')).uncheck(); - await configurationPage.applyButton.click(); - - // verify that contributor can still see "tipa" - const editorPageMember = new EditorPage(memberTab, project); - await editorPageMember.goto(); - await expect(editorPageMember.label('Word', editorPageMember.entryCard)).toHaveCount(2); - await expect(editorPageMember.getTextarea(editorPageMember.entryCard, 'Word', 'th')).toBeVisible(); - await expect(editorPageMember.getTextarea(editorPageMember.entryCard, 'Word', 'tipa')).toBeVisible(); - - // Word then only has "th" visible for manager role - await editorPageManager.goto(); - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(1); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toBeVisible(); - - // restore visibility of "taud" for "Word" field - await configurationPage.goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); - await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', '(Voice)')).check(); - await configurationPage.applyButton.click(); - - // Word has only "th" and "taud" visible for manager role - await editorPageManager.goto(); - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(2); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toBeVisible(); - await expect(editorPageManager.audioPlayer('Word', 'taud')).toBeVisible(); - - // restore visibility of "tipa" input system for manager role - await configurationPage.goto(); - await (await configurationPage.getCheckbox('Input Systems', 'IPA', 'Manager')).check(); - await configurationPage.applyButton.click(); - - // Word has "th", "tipa" and "taud" visible again for manager role - await editorPageManager.goto(); - await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); - await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'tipa')).toBeVisible(); - }); - - }); - - }); -}); diff --git a/test/e2e/tests/editor/editor.audio.spec.ts b/test/e2e/tests/editor/editor.audio.spec.ts new file mode 100644 index 0000000000..ef3e6d99e0 --- /dev/null +++ b/test/e2e/tests/editor/editor.audio.spec.ts @@ -0,0 +1,190 @@ +import { expect } from '@playwright/test'; +import { defaultProject, test } from '../../fixtures'; +import { ConfigurationPageFieldsTab, EditorPage } from '../../pages'; +import { ConfirmModal } from '../../pages/components'; +import { testFilePath } from '../../utils'; + +test.describe('Editor audio', () => { + + const { project, entryIds } = defaultProject(); + + test.beforeAll(async ({ managerTab }) => { + const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project()).goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (IPA)')).check(); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (Voice)')).check(); + await configurationPage.applyButton.click(); + }); + + test.describe('Member', () => { + let editorPageMember: EditorPage; + + test.beforeEach(async ({ memberTab }) => { + editorPageMember = new EditorPage(memberTab, project()); + }); + + test('Audio input system is present, playable and has "more" control (member)', async () => { + await editorPageMember.goto(); + const audio = editorPageMember.getAudioPlayer('Word', 'taud'); + await expect(audio.playIcon).toBeVisible(); + await expect(audio.togglePlaybackAnchor).toBeEnabled(); + + await expect(audio.dropdownToggle).toBeVisible(); + await expect(audio.dropdownToggle).toBeEnabled(); + await expect(audio.uploadButton).not.toBeVisible(); + // this button is only visible when user is observer and has only the right to download + await expect(audio.downloadButton).not.toBeVisible(); + }); + + test('Word 2 (without audio): audio input system is not playable but has "upload" button (member)', async () => { + await editorPageMember.goto({ entryId: entryIds()[1] }); + const audio = editorPageMember.getAudioPlayer('Word', 'taud'); + await expect(audio.togglePlaybackAnchor).not.toBeVisible(); + await expect(audio.dropdownToggle).toBeEnabled(); + await expect(audio.uploadButton).toBeVisible(); + await expect(audio.uploadButton).toBeEnabled(); + await expect(audio.downloadButton).not.toBeVisible(); + }); + }); + + test.describe('Observer', () => { + let editorPageObserver: EditorPage; + + test.beforeEach(async ({ observerTab }) => { + editorPageObserver = new EditorPage(observerTab, project()); + }); + + test('Audio Input System is playable but does not have "more" control (observer)', async () => { + await editorPageObserver.goto(); + const audio = editorPageObserver.getAudioPlayer('Word', 'taud'); + await expect(audio.playIcon).toBeVisible(); + await expect(audio.togglePlaybackAnchor).toBeVisible(); + await expect(audio.togglePlaybackAnchor).toBeEnabled(); + await expect(audio.dropdownToggle).not.toBeVisible(); + await expect(audio.uploadButton).not.toBeVisible(); + await expect(audio.downloadButton).toBeVisible(); + }); + + test('Word 2 (without audio): audio input system is not playable and does not have "upload" button (observer)', async () => { + await editorPageObserver.goto({ entryId: entryIds()[1] }); + const audio = editorPageObserver.getAudioPlayer('Word', 'taud'); + await expect(audio.togglePlaybackAnchor).not.toBeVisible(); + await expect(audio.dropdownToggle).not.toBeVisible(); + await expect(audio.uploadButton).not.toBeVisible(); + await expect(audio.downloadButton).not.toBeVisible(); + }); + }); + + test.describe('Manager', () => { + + let editorPageManager: EditorPage; + + test.beforeEach(async ({ managerTab }) => { + editorPageManager = new EditorPage(managerTab, project()); + }); + + test('Audio input system is present, playable and has "more" control (manager)', async () => { + await editorPageManager.goto(); + const audio = editorPageManager.getAudioPlayer('Word', 'taud'); + await expect(audio.playIcon).toBeVisible(); + await expect(audio.togglePlaybackAnchor).toBeEnabled(); + await expect(audio.dropdownToggle).toBeVisible(); + await expect(audio.dropdownToggle).toBeEnabled(); + await expect(audio.uploadButton).not.toBeVisible(); + // this button is only visible when user is observer and has only the right to download + await expect(audio.downloadButton).not.toBeVisible(); + }); + + test('Slider is present and updates with seeking', async () => { + await editorPageManager.goto(); + const audio = editorPageManager.getAudioPlayer('Word', 'taud'); + await expect(audio.slider).toBeVisible(); + const bounds = await audio.slider.boundingBox(); + const yMiddle = bounds.y + bounds.height / 2; + await editorPageManager.page.mouse.click(bounds.x + 200, yMiddle); + await expect(audio.audioProgressTime).toContainText("0:01 / 0:02"); + }); + + test('File upload drop box is displayed when Upload is clicked & not displayed if upload cancelled (manager)', async () => { + await editorPageManager.goto(); + const dropbox = editorPageManager.entryCard.locator(editorPageManager.dropbox.dragoverFieldSelector); + await expect(dropbox).not.toBeVisible(); + + const cancelAddingAudio = editorPageManager.getCancelDropboxButton(editorPageManager.entryCard, 'Audio'); + await expect(cancelAddingAudio).not.toBeVisible(); + + const audio = editorPageManager.getAudioPlayer('Word', 'taud'); + await audio.dropdownToggle.click(); + await audio.dropdownMenu.uploadReplacementButton.click(); + await expect(audio.dropdownToggle).not.toBeVisible(); + await expect(dropbox).toBeVisible(); + + await expect(cancelAddingAudio).toBeVisible(); + await cancelAddingAudio.click(); + await expect(audio.dropdownToggle).toBeVisible(); + await expect(dropbox).not.toBeVisible(); + await expect(cancelAddingAudio).not.toBeVisible(); + }); + + test('Word 2 (without audio): audio input system is not playable but has "upload" button (manager)', async () => { + await editorPageManager.goto({ entryId: entryIds()[1] }); + const audio = editorPageManager.getAudioPlayer('Word', 'taud'); + await expect(audio.playIcon).not.toBeVisible(); + + await expect(audio.dropdownToggle).not.toBeVisible(); + await expect(audio.uploadButton).toBeVisible(); + await expect(audio.uploadButton).toBeEnabled(); + await expect(audio.downloadButton).not.toBeVisible(); + }); + + test('Can delete audio input system (manager)', async () => { + await editorPageManager.goto(); + const audio = editorPageManager.getAudioPlayer('Word', 'taud'); + await audio.dropdownToggle.click(); + await audio.dropdownMenu.deleteAudioButton.click(); + const confirmModal = new ConfirmModal(editorPageManager.page); + await confirmModal.confirmButton.click(); + await expect(audio.uploadButton).toBeVisible(); + }); + + test('Can\'t upload a non-audio file & can upload audio file', async () => { + // to be independent from the audio deletion test above, go to entry 2 (has no audio) + await editorPageManager.goto({ entryId: entryIds()[1] }); + const noticeElement = editorPageManager.noticeList; + await expect(noticeElement.notices).toHaveCount(0); + + // Can't upload a non-audio file + const audio = editorPageManager.getAudioPlayer('Word', 'taud'); + await audio.uploadButton.click(); + + // Note that Promise.all prevents a race condition between clicking and waiting for the file chooser. + const [fileChooser] = await Promise.all([ + // It is important to call waitForEvent before click to set up waiting. + editorPageManager.page.waitForEvent('filechooser'), + audio.browseButton.click(), + ]); + await fileChooser.setFiles(testFilePath('TestImage.png')); + + await expect(noticeElement.notices).toBeVisible(); + await expect(noticeElement.notices).toContainText(`TestImage.png is not an allowed audio file. Ensure the file is`); + const dropbox = editorPageManager.entryCard.locator(editorPageManager.dropbox.dragoverFieldSelector); + await expect(dropbox).toBeVisible(); + await noticeElement.closeButton.click(); + await expect(noticeElement.notices).toHaveCount(0); + + // Can upload audio file + const [fileChooser2] = await Promise.all([ + editorPageManager.page.waitForEvent('filechooser'), + audio.browseButton.click(), + ]); + await fileChooser2.setFiles(testFilePath('TestAudio.mp3')); + await expect(noticeElement.notices).toHaveCount(1); + await expect(noticeElement.notices).toBeVisible(); + await expect(noticeElement.notices).toContainText('File uploaded successfully'); + await expect(audio.playIcon).toBeVisible(); + await expect(audio.togglePlaybackAnchor).toBeEnabled(); + await expect(audio.dropdownToggle).toBeVisible(); + }); + }); + +}); diff --git a/test/e2e/tests/editor-comments.spec.ts b/test/e2e/tests/editor/editor.comments.spec.ts similarity index 89% rename from test/e2e/tests/editor-comments.spec.ts rename to test/e2e/tests/editor/editor.comments.spec.ts index 200645d2e7..c616f36af6 100644 --- a/test/e2e/tests/editor-comments.spec.ts +++ b/test/e2e/tests/editor/editor.comments.spec.ts @@ -1,25 +1,17 @@ import { expect } from '@playwright/test'; -import { entries } from '../constants'; -import { projectPerTest, test } from '../fixtures'; -import { ConfigurationPageFieldsTab, EditorPage } from '../pages'; +import { test, defaultProject } from '../../fixtures'; +import { ConfigurationPageFieldsTab, EditorPage } from '../../pages'; test.describe('Lexicon Editor Comments', () => { - const project = projectPerTest(); + const { project } = defaultProject(); - test('Creating and viewing comments', async ({ managerTab, projectService }) => { + test('Creating and viewing comments', async ({ managerTab }) => { test.slow(); await test.step('And input systems and entries', async () => { - await projectService.addWritingSystemToProject(project(), 'th-fonipa', 'tipa'); - await projectService.addWritingSystemToProject(project(), 'th-Zxxx-x-audio', 'taud'); - - await projectService.addLexEntry(project(), entries.entry2); - await projectService.addLexEntry(project(), entries.entry1); - await projectService.addLexEntry(project(), entries.multipleMeaningEntry); - const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project()).goto(); - await configurationPage.toggleField('Entry Fields', 'Word'); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'English')).check(); await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (Voice)')).check(); await configurationPage.applyButton.click(); diff --git a/test/e2e/tests/editor/editor.configuration.spec.ts b/test/e2e/tests/editor/editor.configuration.spec.ts new file mode 100644 index 0000000000..a55f041876 --- /dev/null +++ b/test/e2e/tests/editor/editor.configuration.spec.ts @@ -0,0 +1,102 @@ +import { expect } from '@playwright/test'; +import { defaultProject, test } from '../../fixtures'; +import { ConfigurationPageFieldsTab, EditorPage } from '../../pages'; + +test.describe('Editor configuration', async () => { + + const { project } = defaultProject(); + + test.beforeAll(async ({ managerTab }) => { + const configurationPage = new ConfigurationPageFieldsTab(managerTab, project()); + await configurationPage.goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (IPA)')).check(); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (Voice)')).check(); + await configurationPage.applyButton.click(); + }); + + let editorPageManager: EditorPage; + + test.beforeEach(async ({ managerTab }) => { + editorPageManager = new EditorPage(managerTab, project()); + }); + + test('Can change configuration to make a writing system visible or invisible', async ({ managerTab }) => { + await editorPageManager.goto(); + // word has only "th", "tipa" and "taud" visible + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toBeVisible(); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'tipa')).toBeVisible(); + await expect(editorPageManager.audioPlayer('Word', 'taud')).toBeVisible(); + + // make "en" input system visible for "Word" field + const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project()).goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'English')).check(); + await configurationPage.applyButton.click(); + + // check if "en" is visible + await editorPageManager.goto(); + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(4); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'en')).toBeVisible(); + + // make "en" input system invisible for "Word" field + await configurationPage.goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'English')).uncheck(); + await configurationPage.applyButton.click(); + + // check if "en" is invisible + await editorPageManager.goto(); + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'en')).not.toBeVisible(); + }); + + test('Make "taud" input system invisible for "Word" field and "tipa" invisible for manager role, then ensure it worked and change it back', async ({ managerTab, memberTab }) => { + test.slow(); + + const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project()).goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + // Make "taud" input system invisible for "Word" field.... + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', '(Voice)')).uncheck(); + // ....and "tipa" invisible for manager role + await (await configurationPage.getCheckbox('Input Systems', 'IPA', 'Manager')).uncheck(); + await configurationPage.applyButton.click(); + + // verify that contributor can still see "tipa" + const editorPageMember = new EditorPage(memberTab, project()); + await editorPageMember.goto(); + await expect(editorPageMember.label('Word', editorPageMember.entryCard)).toHaveCount(2); + await expect(editorPageMember.getTextarea(editorPageMember.entryCard, 'Word', 'th')).toBeVisible(); + await expect(editorPageMember.getTextarea(editorPageMember.entryCard, 'Word', 'tipa')).toBeVisible(); + + // Word then only has "th" visible for manager role + await editorPageManager.goto(); + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(1); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toBeVisible(); + + // restore visibility of "taud" for "Word" field + await configurationPage.goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', '(Voice)')).check(); + await configurationPage.applyButton.click(); + + // Word has only "th" and "taud" visible for manager role + await editorPageManager.goto(); + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(2); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toBeVisible(); + await expect(editorPageManager.audioPlayer('Word', 'taud')).toBeVisible(); + + // restore visibility of "tipa" input system for manager role + await configurationPage.goto(); + await (await configurationPage.getCheckbox('Input Systems', 'IPA', 'Manager')).check(); + await configurationPage.applyButton.click(); + + // Word has "th", "tipa" and "taud" visible again for manager role + await editorPageManager.goto(); + await expect(editorPageManager.label('Word', editorPageManager.entryCard)).toHaveCount(3); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'tipa')).toBeVisible(); + }); + +}); diff --git a/test/e2e/tests/editor/editor.entries.spec.ts b/test/e2e/tests/editor/editor.entries.spec.ts new file mode 100644 index 0000000000..c734703f26 --- /dev/null +++ b/test/e2e/tests/editor/editor.entries.spec.ts @@ -0,0 +1,259 @@ +import { expect } from '@playwright/test'; +import { entries } from '../../constants'; +import { defaultProject, test } from '../../fixtures'; +import { ConfigurationPageFieldsTab, EditorPage, EntryListPage } from '../../pages'; +import { ConfirmModal } from '../../pages/components'; + +test.describe('Editor entries', () => { + + const { project, entryIds } = defaultProject(); + let editorPageManager: EditorPage; + + test.beforeEach(({ managerTab }) => { + editorPageManager = new EditorPage(managerTab, project()); + }); + + test('Can go from entry editor to entries list', async () => { + await editorPageManager.goto(); + await Promise.all([ + editorPageManager.navigateToEntriesList(), + new EntryListPage(editorPageManager.page, project()).waitFor(), + ]); + }); + + // left side bar entries list + test('Editor page has correct entry count in left side bar entries list', async () => { + await editorPageManager.goto(); + await expect(editorPageManager.compactEntryListItem).toHaveCount(entryIds().length); + }); + + test('URL entry id matches selected entry', async ({ managerTab }) => { + const editorPageManager = await new EditorPage(managerTab, project()).goto({ entryId: entryIds()[1] }); + expect(editorPageManager.page.url()).toContain(entryIds()[1]); + expect(editorPageManager.page.url()).not.toContain(entryIds()[0]); + + await editorPageManager.goto({ entryId: entryIds()[0] }); + expect(editorPageManager.page.url()).toContain(entryIds()[0]); + expect(editorPageManager.page.url()).not.toContain(entryIds()[1]); + + await editorPageManager.goto({ entryId: entryIds()[1] }); + expect(editorPageManager.page.url()).toContain(entryIds()[1]); + expect(editorPageManager.page.url()).not.toContain(entryIds()[0]); + }); + + test('Entry 1: edit page has correct definition, part of speech', async () => { + await editorPageManager.goto(); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard, 'Definition', 'en')) + .toHaveValue(entries.entry1.senses[0].definition.en.value); + expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard, 'Part of Speech')) + .toEqual(entries.entry1.senses[0].partOfSpeech.displayName); + }); + + test('Add citation form as visible field', async ({ managerTab }) => { + const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project()).goto(); + await configurationPage.tabLinks.fields.click(); + await (await configurationPage.getCheckbox('Entry Fields', 'Citation Form', 'Hidden if Empty')).uncheck(); + await configurationPage.applyButton.click(); + await editorPageManager.goto(); + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Citation Form', 'th')).toBeVisible(); + }); + + test('Citation form field overrides lexeme form in dictionary citation view', async ({ managerTab }) => { + const configurationPage = await new ConfigurationPageFieldsTab(managerTab, project()).goto(); + await configurationPage.toggleFieldExpanded('Entry Fields', 'Word'); + await (await configurationPage.getFieldCheckbox('Entry Fields', 'Word', 'ภาษาไทย (IPA)')).check(); + await configurationPage.applyButton.click(); + + await editorPageManager.goto(); + + // Dictionary citation reflects lexeme form when citation form is empty + await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme.th.value, entries.entry1.lexeme.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme['th-fonipa'].value, entries.entry1.lexeme['th-fonipa'].value]); + await expect(editorPageManager.renderedDivs).not.toContainText(['citation form', 'citation form']); + await editorPageManager.showExtraFields(); + const citationFormInput = editorPageManager.getTextarea(editorPageManager.entryCard, 'Citation Form', 'th'); + await citationFormInput.fill('citation form'); + + await expect(editorPageManager.renderedDivs).toContainText(['citation form', 'citation form']); + await expect(editorPageManager.renderedDivs).not.toContainText([entries.entry1.lexeme.th.value, entries.entry1.lexeme.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme['th-fonipa'].value, entries.entry1.lexeme['th-fonipa'].value]); + + await citationFormInput.fill(''); + await expect(editorPageManager.renderedDivs).not.toContainText(['citation form', 'citation form']); + await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme.th.value, entries.entry1.lexeme.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.entry1.lexeme['th-fonipa'].value, entries.entry1.lexeme['th-fonipa'].value]); + }); + + test('Navigate to other entries with left entry bar', async () => { + await editorPageManager.goto({ entryId: entryIds()[1] }); + + await Promise.all([ + editorPageManager.page.locator('text=' + entries.multipleMeaningEntry.senses[0].definition.en.value).click(), + editorPageManager.page.waitForURL(editorPageManager.entryUrl(entryIds()[2])), + ]); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first(), 'Definition', 'en')).toHaveValue(entries.multipleMeaningEntry.senses[0].definition.en.value); + }); + + test('Word 2: edit page has correct definition, part of speech', async () => { + await editorPageManager.goto({ entryId: entryIds()[1] }); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard, 'Definition', 'en')) + .toHaveValue(entries.entry2.senses[0].definition.en.value); + + expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard, 'Part of Speech')) + .toEqual(entries.entry2.senses[0].partOfSpeech.displayName); + }); + + test('Dictionary citation reflects example sentences and translations', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[0].sentence.th.value, entries.multipleMeaningEntry.senses[0].examples[0].sentence.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[0].translation.en.value, entries.multipleMeaningEntry.senses[0].examples[0].translation.en.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[1].sentence.th.value, entries.multipleMeaningEntry.senses[0].examples[1].sentence.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[0].examples[1].translation.en.value, entries.multipleMeaningEntry.senses[0].examples[1].translation.en.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[0].sentence.th.value, entries.multipleMeaningEntry.senses[1].examples[0].sentence.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[0].translation.en.value, entries.multipleMeaningEntry.senses[1].examples[0].translation.en.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[1].sentence.th.value, entries.multipleMeaningEntry.senses[1].examples[1].sentence.th.value]); + await expect(editorPageManager.renderedDivs).toContainText([entries.multipleMeaningEntry.senses[1].examples[1].translation.en.value, entries.multipleMeaningEntry.senses[1].examples[1].translation.en.value]); + }); + + test('Word with multiple definitions: edit page has correct definitions, parts of speech', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first(), 'Definition', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].definition.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1), 'Definition', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].definition.en.value); + + expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard.nth(0), 'Part of Speech')) + .toEqual(entries.multipleMeaningEntry.senses[0].partOfSpeech.displayName); + expect(await editorPageManager.getSelectedValueFromSelectDropdown(editorPageManager.senseCard.nth(1), 'Part of Speech')) + .toEqual(entries.multipleMeaningEntry.senses[1].partOfSpeech.displayName); + }); + + test('Word with multiple meanings: edit page has correct example sentences, translations', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Sentence', 'th')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[0].sentence.th.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Translation', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[0].translation.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Sentence', 'th')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[1].sentence.th.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first().locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Translation', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].examples[1].translation.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Sentence', 'th')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[0].sentence.th.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=0'), 'Translation', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[0].translation.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Sentence', 'th')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[1].sentence.th.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1).locator(editorPageManager.exampleCardSelector + ' >> nth=1'), 'Translation', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].examples[1].translation.en.value); + }); + + test('While Show Hidden Fields has not been clicked, hidden fields are hidden if they are empty', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(0), 'Semantics Note', 'en')).toHaveCount(0); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(0), 'General Note', 'en')).toBeVisible(); + await editorPageManager.showExtraFields(); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(0), 'Semantics Note', 'en')).toBeVisible(); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(0), 'General Note', 'en')).toBeVisible(); + }); + + test('Word with multiple meanings: edit page has correct general notes, sources', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(0), 'General Note', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].generalNote.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1), 'General Note', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].generalNote.en.value); + await editorPageManager.showExtraFields(); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(0), 'Source', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].source.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1), 'Source', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].source.en.value); + }); + + test('Senses can be reordered and deleted', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + await editorPageManager.senseCard.first().locator(editorPageManager.moveDownButtonSelector).first().click(); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.first(), 'Definition', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[1].definition.en.value); + await expect(editorPageManager.getTextarea( + editorPageManager.senseCard.nth(1), 'Definition', 'en')) + .toHaveValue(entries.multipleMeaningEntry.senses[0].definition.en.value); + }); + + test('Create new word, modify new word, autosaves changes, new word visible in editor and list', async () => { + await editorPageManager.goto({ entryId: entryIds()[2] }); + + const startEntryCount = await editorPageManager.compactEntryListItem.count(); + + await editorPageManager.entryList.createNewWordButton.click(); + + const newEntryCount = startEntryCount + 1; + await expect(editorPageManager.compactEntryListItem).toHaveCount(newEntryCount); + + // go back to editor + await (editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')) + .fill(entries.entry3.lexeme.th.value); + await (editorPageManager.getTextarea(editorPageManager.senseCard, 'Definition', 'en')) + .fill(entries.entry3.senses[0].definition.en.value); + + const partOfSpeedDropdown = editorPageManager.getDropdown(editorPageManager.senseCard, 'Part of Speech'); + partOfSpeedDropdown.selectOption({ label: 'Noun (n)' }); + + // Autosaves changes + await editorPageManager.page.waitForURL(url => !url.hash.includes('editor/entry/_new_')); + await editorPageManager.page.reload(); + + await expect(partOfSpeedDropdown).toHaveSelectedOption({ label: 'Noun (n)' }); + + const alreadyThere: string = await editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th').inputValue(); + await (editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')) + .fill(alreadyThere + 'a'); + await editorPageManager.page.reload(); + await expect((editorPageManager.getTextarea( + editorPageManager.entryCard, 'Word', 'th'))) + .toHaveValue(entries.entry3.lexeme.th.value + 'a'); + await (editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')) + .fill(entries.entry3.lexeme.th.value); + + // New word is visible in list + await editorPageManager.entryList.filterInput.fill(entries.entry3.senses[0].definition.en.value); + await expect(editorPageManager.entryList.matchCount).toContainText(/1(?= \/)/); + await editorPageManager.entryList.filterInputClearButton.click(); + + // remove new word to restore original word count + await editorPageManager.entryCard.first().locator(editorPageManager.deleteCardButtonSelector).first().click(); + const confirmModal = new ConfirmModal(editorPageManager.page); + await confirmModal.confirmButton.click(); + + await expect(editorPageManager.compactEntryListItem).toHaveCount(startEntryCount); + + // previous entry is selected after delete + await expect(editorPageManager.getTextarea( + editorPageManager.entryCard, 'Word', 'th')) + .toHaveValue(entries.entry1.lexeme.th.value); + }); +}); diff --git a/test/e2e/tests/editor/editor.pictures.spec.ts b/test/e2e/tests/editor/editor.pictures.spec.ts new file mode 100644 index 0000000000..a8cae945d1 --- /dev/null +++ b/test/e2e/tests/editor/editor.pictures.spec.ts @@ -0,0 +1,133 @@ +import { expect } from '@playwright/test'; +import { entries } from '../../constants'; +import { defaultProject, projectPerTest, test } from '../../fixtures'; +import { ConfigurationPageFieldsTab, EditorPage } from '../../pages'; +import { ConfirmModal } from '../../pages/components'; +import { Project } from '../../utils'; + +test.describe('Editor pictures', () => { + + const { project } = defaultProject(); + const newProject = projectPerTest(true); + + let editorPageManager: EditorPage; + + test.beforeEach(({ managerTab }) => { + editorPageManager = new EditorPage(managerTab, project()); + }); + + test('First picture and caption is present', async ({ projectService }) => { + const screenshotProject: Project = await newProject(); + await projectService.addLexEntry(screenshotProject.code, entries.entry1); + await projectService.addPictureFileToProject(screenshotProject, entries.entry1.senses[0].pictures[0].fileName); + + const editorPagePicture = await new EditorPage(editorPageManager.page, screenshotProject).goto(); + const picture = editorPagePicture.picture(entries.entry1.senses[0].pictures[0].fileName); + const img = await picture.elementHandle(); + await expect(editorPagePicture.page).toHaveScreenshot({ clip: await img.boundingBox() }); + const caption = editorPagePicture.caption(picture); + await expect(caption).toHaveValue(entries.entry1.senses[0].pictures[0].caption.en.value); + }); + + test('File upload drop box is displayed when Add Picture is clicked and can be cancelled', async () => { + await editorPageManager.goto(); + const addPictureButton = editorPageManager.senseCard.locator(editorPageManager.addPictureButtonSelector); + await expect(addPictureButton).toBeVisible(); + const dropbox = editorPageManager.senseCard.locator(editorPageManager.dropbox.dragoverFieldSelector); + await expect(dropbox).not.toBeVisible(); + const cancelAddingPicture = editorPageManager.getCancelDropboxButton(editorPageManager.senseCard, 'Picture'); + await expect(cancelAddingPicture).not.toBeVisible(); + + await addPictureButton.click(); + await expect(addPictureButton).not.toBeVisible(); + await expect(dropbox).toBeVisible(); + await expect(cancelAddingPicture).toBeVisible(); + + // file upload drop box is not displayed when Cancel Adding Picture is clicked + await cancelAddingPicture.click(); + await expect(addPictureButton).toBeVisible(); + await expect(dropbox).not.toBeVisible(); + await expect(cancelAddingPicture).not.toBeVisible(); + }); + + test('Showing and hiding captions', async ({ managerTab, browserName }) => { + test.slow(browserName === 'firefox'); + + const configurationPage = new ConfigurationPageFieldsTab(managerTab, project()); + + await test.step('Hide empty captions', async () => { + await configurationPage.goto(); + await configurationPage.tabLinks.fields.click(); + await configurationPage.toggleFieldExpanded('Meaning Fields', 'Pictures'); + await (await configurationPage.getFieldCheckbox('Meaning Fields', 'Pictures', 'Hide Caption If Empty')).check(); + await configurationPage.applyButton.click(); + }); + + const caption = await test.step('Non-empty caption is visible', async () => { + await editorPageManager.goto(); + await editorPageManager.showExtraFields(false); + const picture = editorPageManager.picture(entries.entry1.senses[0].pictures[0].fileName); + const caption = editorPageManager.caption(picture); + await expect(caption).toBeVisible(); + return caption; + }); + + await test.step('Empty caption is hidden', async () => { + await expect(caption).toBeVisible(); // it disappears immediately which could be annoying + await caption.fill(''); + await expect(caption).not.toBeVisible(); // it disappears immediately which could be annoying + await editorPageManager.reload(); + await expect(caption).not.toBeVisible(); + }); + + await test.step('Show empty captions', async () => { + await configurationPage.goto(); + await configurationPage.tabLinks.fields.click(); + await configurationPage.toggleFieldExpanded('Meaning Fields', 'Pictures'); + await (await configurationPage.getFieldCheckbox('Meaning Fields', 'Pictures', 'Hide Caption If Empty')).uncheck(); + await configurationPage.applyButton.click(); + }); + + await test.step('Empty caption is visible', async () => { + await editorPageManager.goto(); + await editorPageManager.showExtraFields(false); + const picture = editorPageManager.picture(entries.entry1.senses[0].pictures[0].fileName); + const caption = editorPageManager.caption(picture); + await expect(caption).toBeVisible(); + }); + }); + + test('Picture is removed when Delete is clicked & can change config to hide pictures and hide captions', async ({ projectService }) => { + const testProject: Project = await newProject(); + await projectService.addLexEntry(testProject, entries.entry1); + await projectService.addPictureFileToProject(testProject, entries.entry1.senses[0].pictures[0].fileName); + const editorPagePicture = await new EditorPage(editorPageManager.page, testProject).goto(); + + // Picture is removed when Delete is clicked + let picture = editorPagePicture.picture(entries.entry1.senses[0].pictures[0].fileName); + await editorPagePicture.deletePictureButton(entries.entry1.senses[0].pictures[0].fileName).click(); + const confirmModal = new ConfirmModal(editorPagePicture.page); + await confirmModal.confirmButton.click(); + picture = editorPagePicture.picture(entries.entry1.senses[0].pictures[0].fileName); + await expect(picture).not.toBeVisible(); + + const configurationPage = await new ConfigurationPageFieldsTab(editorPageManager.page, testProject).goto(); + await configurationPage.tabLinks.fields.click(); + await (await configurationPage.getCheckbox('Meaning Fields', 'Pictures', 'Hidden if Empty')).check(); + await configurationPage.toggleFieldExpanded('Meaning Fields', 'Pictures'); + await (await configurationPage.getFieldCheckbox('Meaning Fields', 'Pictures', 'Hide Caption If Empty')).uncheck(); + await configurationPage.applyButton.click(); + + // can change config to hide pictures and hide captions + await editorPagePicture.goto(); + picture = editorPagePicture.picture(entries.entry1.senses[0].pictures[0].fileName); + await expect(picture).not.toBeVisible(); + expect(editorPagePicture.getPicturesOuterDiv(editorPagePicture.senseCard)).not.toBeVisible(); + await editorPagePicture.showExtraFields(); + expect(editorPagePicture.getPicturesOuterDiv(editorPagePicture.senseCard)).toBeVisible(); + await editorPagePicture.showExtraFields(false); + expect(editorPagePicture.getPicturesOuterDiv(editorPagePicture.senseCard)).not.toBeVisible(); + picture = editorPagePicture.picture(entries.entry1.senses[0].pictures[0].fileName); + await expect(picture).not.toBeVisible(); + }); +}); diff --git a/test/e2e/tests/entry-list.spec.ts b/test/e2e/tests/entry-list.spec.ts new file mode 100644 index 0000000000..2a437f9988 --- /dev/null +++ b/test/e2e/tests/entry-list.spec.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test'; +import { entries } from '../constants'; +import { defaultProject, test } from '../fixtures'; +import { EditorPage, EntryListPage } from '../pages'; + +test.describe('Entries List', () => { + + const { project, entryIds } = defaultProject(); + + test.describe('Entries List', () => { + + let entryListPageManager: EntryListPage; + + test.beforeEach(async ({ managerTab }) => { + entryListPageManager = await EntryListPage.goto(managerTab, project()); + }); + + test('Entries list has correct number of entries', async () => { + await entryListPageManager.expectTotalNumberOfEntries(entryIds().length); + }); + + test('Search function works correctly', async () => { + await entryListPageManager.filterInput.fill('asparagus'); + await expect(entryListPageManager.matchCount).toContainText(/1(?= \/)/); + + // remove filter, filter again, have same result + await entryListPageManager.filterInputClearButton.click(); + await entryListPageManager.filterInput.fill('asparagus'); + await expect(entryListPageManager.matchCount).toContainText(/1(?= \/)/); + await entryListPageManager.filterInputClearButton.click(); + }); + + test('Can click on first entry', async () => { + const [, editorPageManager] = await Promise.all([ + entryListPageManager.entry(entries.entry1.lexeme.th.value).click(), + new EditorPage(entryListPageManager.page, project()).waitFor(), + ]) + await expect(editorPageManager.getTextarea(editorPageManager.entryCard, 'Word', 'th')).toHaveValue(entries.entry1.lexeme.th.value); + }); + + test('Add word buttons works and redirects to editor', async () => { + const entryCount = await entryListPageManager.entries.count(); + const [editorPageManager] = await Promise.all([ + EditorPage.waitFor(entryListPageManager.page, project()), + entryListPageManager.createNewWordButton.click(), + ]); + + const newEntryCount = entryCount + 1; + await expect(editorPageManager.compactEntryListItem).toHaveCount(newEntryCount); + + await Promise.all([ + editorPageManager.navigateToEntriesList(), + entryListPageManager.waitFor(), + ]); + + await entryListPageManager.expectTotalNumberOfEntries(newEntryCount); + }); + + }); +}); diff --git a/test/e2e/tests/reset-forgotten-password.spec.ts b/test/e2e/tests/reset-forgotten-password.spec.ts index ac9c5d888c..9d285a6f86 100644 --- a/test/e2e/tests/reset-forgotten-password.spec.ts +++ b/test/e2e/tests/reset-forgotten-password.spec.ts @@ -5,6 +5,8 @@ import { ForgotPasswordPage, LoginPage, ProjectsPage, ResetPasswordPage, SignupP test.describe('Reset forgotten password', () => { test('User can reset password', async ({ tab, userService }) => { + test.slow(); + const time = Date.now(); const user = { name: `Reset password user - ${time}`, diff --git a/test/e2e/tests/user-profile.spec.ts b/test/e2e/tests/user-profile.spec.ts index c663056fd0..391dc8decb 100644 --- a/test/e2e/tests/user-profile.spec.ts +++ b/test/e2e/tests/user-profile.spec.ts @@ -3,8 +3,7 @@ import { test } from '../fixtures'; import { LoginPage, ProjectsPage, UserProfilePage } from '../pages'; import { login, UserDetails } from '../utils'; -// Potentially flaky in CI. We're investigating. -test.describe.fixme('User Profile', () => { +test.describe('User Profile', () => { test('Generated user account and about me info', async ({ tab, userService }) => { const user = await userService.createRandomUser(); diff --git a/test/e2e/utils/project-utils.ts b/test/e2e/utils/project-utils.ts index e66f6e4608..20314d73ed 100644 --- a/test/e2e/utils/project-utils.ts +++ b/test/e2e/utils/project-utils.ts @@ -1,9 +1,14 @@ import { APIRequestContext, TestInfo } from "@playwright/test"; -import { Project, UserDetails } from "./types"; +import { Project, TestProject, UserDetails } from "./types"; import { getTestControl, TestControlService } from './test-control-api'; import { TestFile } from "./path-utils"; +import { users, entries } from "../constants"; -const toProjectCode = (name: string): string => name.toLowerCase().replace(/[ \.]/g, '_'); +const toProjectCode = (name: string): string => { + name = name.toLowerCase().replace(/[ \.]/g, '_'); + const slash = name.indexOf('/'); + return name.substring(slash + 1); +} export const toProject = (name: string, id?: string): Project => ({ name, @@ -65,4 +70,19 @@ export class ProjectTestService { if (data.id == null) data.id = ''; return this.call('add_lexical_entry', [getCode(projectCode), data]) as Promise; } + + async createDefaultProject(testInfo: TestInfo): Promise { + const project = await this.initTestProject(testInfo.titlePath[0], undefined, users.manager, [users.member]); + await this.addUserToProject(project, users.observer, "observer"); + await this.addWritingSystemToProject(project, 'th-fonipa', 'tipa'); + await this.addWritingSystemToProject(project, 'th-Zxxx-x-audio', 'taud'); + await this.addPictureFileToProject(project, entries.entry1.senses[0].pictures[0].fileName); + await this.addAudioVisualFileToProject(project, entries.entry1.lexeme['th-Zxxx-x-audio'].value); + const projectEntries = await Promise.all([ + this.addLexEntry(project.code, entries.entry1), + this.addLexEntry(project.code, entries.entry2), + this.addLexEntry(project.code, entries.multipleMeaningEntry), + ]); + return { project: () => project, entryIds: () => projectEntries }; + } } diff --git a/test/e2e/utils/test-control-api.ts b/test/e2e/utils/test-control-api.ts index a8bdf43b09..573e283ab2 100644 --- a/test/e2e/utils/test-control-api.ts +++ b/test/e2e/utils/test-control-api.ts @@ -28,7 +28,7 @@ async function jsonRpc(request: APIRequestContext, url: string, method: string, throw json.error; } } catch (error) { - console.log(await result.text()); + console.error(await result.text()); throw error; } } diff --git a/test/e2e/utils/types.ts b/test/e2e/utils/types.ts index 50857a9c88..a7810b9a2d 100644 --- a/test/e2e/utils/types.ts +++ b/test/e2e/utils/types.ts @@ -10,3 +10,8 @@ export type Project = { code: string, id: string } + +export type TestProject = { + project(): Project, + entryIds(): string[], +}