From 9918a40401bb07035f94a95f55827cbcceac9ddb Mon Sep 17 00:00:00 2001 From: billy clark Date: Tue, 15 Nov 2022 04:21:28 -0500 Subject: [PATCH 01/10] refactor activity call into consistent convention (#1593) --- .../projects/[project_code]/activities/+server.ts | 11 ++++++----- src/Api/Model/Shared/Dto/RightsHelper.php | 3 --- src/Api/Service/Sf.php | 10 ---------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/next-app/src/routes/projects/[project_code]/activities/+server.ts b/next-app/src/routes/projects/[project_code]/activities/+server.ts index be00ec015d..638bb72241 100644 --- a/next-app/src/routes/projects/[project_code]/activities/+server.ts +++ b/next-app/src/routes/projects/[project_code]/activities/+server.ts @@ -4,23 +4,24 @@ import { sf } from '$lib/fetch/server' export async function GET({ params: { project_code }, request: { headers } }) { const cookie = headers.get('cookie') - const activities = await get_activities({ project_code, cookie }) + await sf({ name: 'set_project', args: [ project_code ], cookie }) + + const activities = await get_activities({ cookie }) return json(activities) } // src/Api/Model/Shared/Dto/ActivityListDto.php // src/Api/Model/Shared/Dto/ActivityListDto.php->ActivityListModel.__construct -export async function get_activities({ project_code, cookie, start_date, end_date }) { +export async function get_activities({ cookie, start_date, end_date }) { const args = { - name: 'activity_list_dto_for_project', + name: 'activity_list_dto_for_current_project', args: [ - project_code, { startDate: start_date, endDate: end_date, limit: start_date || end_date ? 50 : 0, - } + }, ], cookie, } diff --git a/src/Api/Model/Shared/Dto/RightsHelper.php b/src/Api/Model/Shared/Dto/RightsHelper.php index 3434bf80bb..47252d1ed8 100644 --- a/src/Api/Model/Shared/Dto/RightsHelper.php +++ b/src/Api/Model/Shared/Dto/RightsHelper.php @@ -210,9 +210,6 @@ public function userCanAccessMethod($methodName) case "activity_list_dto_for_current_project": return $this->userHasSiteRight(Domain::PROJECTS + Operation::VIEW_OWN); - case "activity_list_dto_for_project": - return $this->userHasSiteRight(Domain::PROJECTS + Operation::VIEW_OWN); - case "activity_list_dto_for_lexical_entry": return $this->userHasProjectRight(Domain::ENTRIES + Operation::VIEW); diff --git a/src/Api/Service/Sf.php b/src/Api/Service/Sf.php index 52b371d82e..006a6ad8bb 100644 --- a/src/Api/Service/Sf.php +++ b/src/Api/Service/Sf.php @@ -438,16 +438,6 @@ public function activity_list_dto_for_current_project($filterParams = []) return ActivityListDto::getActivityForOneProject($projectModel, $this->userId, $filterParams); } - public function activity_list_dto_for_project($projectCode, $filterParams = []) - { - $projectModel = ProjectModel::getByProjectCode($projectCode); - $user = new UserModel($this->userId); - if ($user->isMemberOfProject($projectModel->id->asString())) { - return ActivityListDto::getActivityForOneProject($projectModel, $this->userId, $filterParams); - } - throw new UserUnauthorizedException("User $this->userId is not a member of project $projectCode"); - } - public function activity_list_dto_for_lexical_entry($entryId, $filterParams = []) { $projectModel = ProjectModel::getById($this->projectId); From 0047e99bd554e66723f5b8a3523bc0a61f70eeae Mon Sep 17 00:00:00 2001 From: billy clark Date: Tue, 15 Nov 2022 17:07:08 -0500 Subject: [PATCH 02/10] added missing punctuation (#1598) --- .../languageforge/lexicon/editor/editor.component.ts | 2 +- .../languageforge/lexicon/editor/field/dc-audio.component.ts | 2 +- .../languageforge/lexicon/editor/field/dc-entry.component.ts | 4 ++-- .../lexicon/editor/field/dc-picture.component.ts | 2 +- .../languageforge/lexicon/editor/field/dc-sense.component.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/angular-app/languageforge/lexicon/editor/editor.component.ts b/src/angular-app/languageforge/lexicon/editor/editor.component.ts index b12c648369..0b7f66c593 100644 --- a/src/angular-app/languageforge/lexicon/editor/editor.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/editor.component.ts @@ -520,7 +520,7 @@ export class LexiconEditorController implements angular.IController { deleteEntry = (entry: LexEntry): void => { const deleteMsg = 'Are you sure you want to delete the entry \'' + - LexiconUtilityService.getLexeme(this.lecConfig, this.lecConfig.entry, entry) + '\''; + LexiconUtilityService.getLexeme(this.lecConfig, this.lecConfig.entry, entry) + '\'?'; this.modal.showModalSimple('Delete Entry', deleteMsg, 'Cancel', 'Delete Entry').then(() => { let iShowList = this.editorService.getIndexInList(entry.id, this.visibleEntries); this.editorService.removeEntryFromLists(entry.id); diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts index aad11db1c6..5d9cde0916 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts @@ -71,7 +71,7 @@ export class FieldAudioController implements angular.IController { deleteAudio(): void { if (this.hasAudio()) { const deleteMsg = 'Are you sure you want to delete the audio \'' + - FieldAudioController.originalFileName(this.dcFilename) + '\''; + FieldAudioController.originalFileName(this.dcFilename) + '\'?'; this.modalService.showModalSimple('Delete Audio', deleteMsg, 'Cancel', 'Delete Audio') .then(() => { this.lexProjectService.removeMediaFile('audio', this.dcFilename, result => { diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.ts index 06ec517228..bdcf299bcc 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.ts @@ -52,9 +52,9 @@ export class FieldEntryController implements angular.IController { } deleteSense = (index: number): void => { - const deletemsg = 'Are you sure you want to delete the meaning \' ' + + const deletemsg = 'Are you sure you want to delete the meaning \'' + LexiconUtilityService.getMeaning(this.control.config, this.config.fields.senses as LexConfigFieldList, - this.model.senses[index]) + ' \''; + this.model.senses[index]) + '\'?'; this.modal.showModalSimple('Delete Meaning', deletemsg, 'Cancel', 'Delete Meaning') .then(() => { // Adding or removing senses makes for a non-delta update, so save a possible delta update first diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts index e3b0a4dcd4..5d978ea29e 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts @@ -72,7 +72,7 @@ export class FieldPictureController implements angular.IController { const fileName: string = this.pictures[index].fileName; if (fileName) { const deleteMsg: string = 'Are you sure you want to delete the picture \'' + - FieldPictureController.originalFileName(fileName) + '\''; + FieldPictureController.originalFileName(fileName) + '\'?'; this.modalService.showModalSimple('Delete Picture', deleteMsg, 'Cancel', 'Delete Picture').then(() => { this.pictures.splice(index, 1); this.lexProjectService.removeMediaFile('sense-image', fileName, result => { diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.ts index 3fd4439714..028c577379 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.ts @@ -67,10 +67,10 @@ export class FieldSenseController implements angular.IController { // noinspection JSUnusedGlobalSymbols deleteExample = (index: number): void => { - const deletemsg = 'Are you sure you want to delete the example \' ' + + const deletemsg = 'Are you sure you want to delete the example \'' + LexiconUtilityService.getExample(this.control.config, this.config.fields.examples as LexConfigFieldList, this.model.examples[index], 'sentence') - + ' \''; + + '\'?'; this.modal.showModalSimple('Delete Example', deletemsg, 'Cancel', 'Delete Example') .then(() => { // Adding or removing examples makes for a non-delta update, so save a possible delta update first From 1adf582ae982012f9afbdd7baca79efae55f14f1 Mon Sep 17 00:00:00 2001 From: billy clark Date: Tue, 15 Nov 2022 17:09:16 -0500 Subject: [PATCH 03/10] utilize HTML `title` for tooltips (#1597) --- .../lexicon/editor/field/dc-entry.component.html | 2 +- .../lexicon/editor/field/dc-example.component.html | 6 +++--- .../lexicon/editor/field/dc-sense.component.html | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.html b/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.html index ddf3050ca1..832a52cef0 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.html +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-entry.component.html @@ -4,7 +4,7 @@
Entry - +
diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-example.component.html b/src/angular-app/languageforge/lexicon/editor/field/dc-example.component.html index ce295a38f5..a2b940711b 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-example.component.html +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-example.component.html @@ -6,15 +6,15 @@ - + diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.html b/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.html index 1fba4075fb..7201e10671 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.html +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-sense.component.html @@ -6,15 +6,15 @@ - + From 538ade24389c1fc917b7c441603eda0450cd6061 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 17 Nov 2022 17:36:34 +0700 Subject: [PATCH 04/10] Migrate user-profile and lexicon project settings tests to playwright (#1557) * Remove data-testid attribute * Refactor global-setup and add writeable test user * Fix NPE when configuring field visibility at user level * Move playwright.config.ts to test folder This way it can reference code (see next commit), but this also seems like the natural place for it. * Refactor tests with new toHaveSelectedOption matcher * Finish/Add project-settings and user-profile playwright tests --- Makefile | 4 +- package.json | 2 +- .../configuration-option-lists.component.ts | 2 +- .../configuration/field-unified-view.model.ts | 4 +- .../settings/project-settings.component.html | 2 +- .../lexicon-project-settings.e2e-spec.ts | 15 --- test/app/testConstants.json | 4 + test/e2e/pages/base-page.ts | 2 +- test/e2e/pages/editor.page.ts | 12 +- test/e2e/pages/project-settings.page.ts | 15 ++- test/e2e/pages/user-profile.page.ts | 27 ++++- .../e2e/playwright.config.ts | 11 +- test/e2e/project-settings.spec.ts | 10 +- test/e2e/semantic-domains.spec.ts | 28 +++-- test/e2e/types/index.d.ts | 9 ++ test/e2e/user-profile.spec.ts | 113 ++++++++++++++++++ test/e2e/utils/e2e-users.ts | 10 ++ test/e2e/utils/fixtures.ts | 46 +++---- test/e2e/utils/globalSetup.ts | 36 +----- test/e2e/utils/login.ts | 6 +- test/e2e/utils/playwright-helpers.ts | 44 ++++--- test/e2e/utils/user-tools.ts | 32 +++++ test/e2e/utils/userFixtures.ts | 15 --- 23 files changed, 307 insertions(+), 142 deletions(-) delete mode 100644 test/app/languageforge/lexicon/settings/lexicon-project-settings.e2e-spec.ts rename playwright.config.ts => test/e2e/playwright.config.ts (92%) create mode 100644 test/e2e/types/index.d.ts create mode 100644 test/e2e/user-profile.spec.ts create mode 100644 test/e2e/utils/e2e-users.ts create mode 100644 test/e2e/utils/user-tools.ts delete mode 100644 test/e2e/utils/userFixtures.ts diff --git a/Makefile b/Makefile index 1b79ec7434..eacebbde7b 100644 --- a/Makefile +++ b/Makefile @@ -13,14 +13,14 @@ dev: start playwright-tests-ci: npm ci $(MAKE) playwright-app - npx playwright install chromium && npx playwright test + npx playwright install chromium && npx playwright test -c ./test/e2e/playwright.config.ts .PHONY: playwright-tests playwright-tests: npm install $(MAKE) playwright-app docker compose up -d ui-builder - npx playwright install chromium && npx playwright test $(params) + npx playwright install chromium && npx playwright test -c ./test/e2e/playwright.config.ts $(params) .PHONY: playwright-app playwright-app: diff --git a/package.json b/package.json index fbaba18850..aeb429d5d7 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "webpack:dev:watch": "webpack -w --config webpack-dev.config.js", "webpack:prd": "webpack --config webpack-prd.config.js", "compile-test-e2e": "tsc -p test/app", - "test-e2e": "protractor test/app/protractorConf.js", + "test-e2e": "npx playwright test -c ./test/e2e/playwright.config.ts", "prepare": "husky install" }, "license": "MIT", diff --git a/src/angular-app/languageforge/lexicon/settings/configuration/configuration-option-lists.component.ts b/src/angular-app/languageforge/lexicon/settings/configuration/configuration-option-lists.component.ts index 6a5c1c9cb3..d63c0ea762 100644 --- a/src/angular-app/languageforge/lexicon/settings/configuration/configuration-option-lists.component.ts +++ b/src/angular-app/languageforge/lexicon/settings/configuration/configuration-option-lists.component.ts @@ -18,7 +18,7 @@ export class OptionListConfigurationController implements angular.IController { return null; } - return this.olcOptionListsDirty[this.currentListIndex].items; + return this.olcOptionListsDirty[this.currentListIndex]?.items; }, (newVal: LexOptionListItem[], oldVal: LexOptionListItem[]) => { if (newVal != null && newVal !== oldVal) { diff --git a/src/angular-app/languageforge/lexicon/settings/configuration/field-unified-view.model.ts b/src/angular-app/languageforge/lexicon/settings/configuration/field-unified-view.model.ts index b7c1bfe018..9b2070ba4f 100644 --- a/src/angular-app/languageforge/lexicon/settings/configuration/field-unified-view.model.ts +++ b/src/angular-app/languageforge/lexicon/settings/configuration/field-unified-view.model.ts @@ -422,9 +422,9 @@ export class ConfigurationFieldUnifiedViewModel { const userView: LexUserViewConfig = config.userViews[userId]; if (userView != null) { if (userView.inputSystems && userView.inputSystems.length) { - inputSystemSettings.groups[groupIndex].show = userView.inputSystems.includes(tag); + inputSystemSettings.groups[groupIndex] = {show: userView.inputSystems.includes(tag)}; } else { - inputSystemSettings.groups[groupIndex].show = true; + inputSystemSettings.groups[groupIndex] = {show: true}; } } } diff --git a/src/angular-app/languageforge/lexicon/settings/project-settings.component.html b/src/angular-app/languageforge/lexicon/settings/project-settings.component.html index 404c81491e..d010c1a0be 100644 --- a/src/angular-app/languageforge/lexicon/settings/project-settings.component.html +++ b/src/angular-app/languageforge/lexicon/settings/project-settings.component.html @@ -27,7 +27,7 @@
-
+
{{$ctrl.project.ownerRef.username}}
diff --git a/test/app/languageforge/lexicon/settings/lexicon-project-settings.e2e-spec.ts b/test/app/languageforge/lexicon/settings/lexicon-project-settings.e2e-spec.ts deleted file mode 100644 index 2344010940..0000000000 --- a/test/app/languageforge/lexicon/settings/lexicon-project-settings.e2e-spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {BellowsLoginPage} from '../../../bellows/shared/login.page'; -import {ProjectSettingsPage} from '../shared/project-settings.page'; - -describe('Lexicon E2E Project Settings', () => { - const constants = require('../../../testConstants.json'); - const loginPage = new BellowsLoginPage(); - const projectSettingsPage = new ProjectSettingsPage(); - - it('should display project properties for manager', async () => { - await loginPage.loginAsManager(); - await projectSettingsPage.get(constants.testProjectName); - expect(await projectSettingsPage.tabs.project.isDisplayed()); - expect(await projectSettingsPage.projectTab.saveButton.isDisplayed()).toBe(true); - }); -}); diff --git a/test/app/testConstants.json b/test/app/testConstants.json index 4c0a2c68bf..a9c4c7ba29 100644 --- a/test/app/testConstants.json +++ b/test/app/testConstants.json @@ -36,6 +36,10 @@ "observerName": "Test ObserverUser", "observerPassword": "normaluser5", "observerEmail": "test_runner_observer_normal_user@example.com", + "writableUsername": "test_runner_writable_user", + "writableName": "Test WritableUser", + "writablePassword": "writableuser5", + "writableEmail": "test_runner_writable_user@example.com", "unusedUsername": "test_runner_unused_user", "unusedName": "Test UnusedUser", "unusedEmail": "test_runner_unused_user+test@example.com", diff --git a/test/e2e/pages/base-page.ts b/test/e2e/pages/base-page.ts index 6ffef62a12..76ef0fb8c8 100644 --- a/test/e2e/pages/base-page.ts +++ b/test/e2e/pages/base-page.ts @@ -19,7 +19,7 @@ export abstract class BasePage { async waitForPage(): Promise { await Promise.all([ - this.page.waitForNavigation({ url: new RegExp(`${this.url}(#|$)`) }), + this.page.waitForNavigation({url: new RegExp(`${this.url}(#|$)`)}), this.waitFor?.waitFor(), ]); } diff --git a/test/e2e/pages/editor.page.ts b/test/e2e/pages/editor.page.ts index 76f41ab255..3f5c20cd34 100644 --- a/test/e2e/pages/editor.page.ts +++ b/test/e2e/pages/editor.page.ts @@ -3,6 +3,7 @@ import { Project } from '../utils/types'; import { BasePage, GotoOptions } from './base-page'; import { ConfigurationPage } from './configuration.page'; import { EntriesListPage } from './entries-list.page'; +import { ProjectSettingsPage } from './project-settings.page'; export interface EditorGotoOptions extends GotoOptions { entryId?: string; @@ -67,7 +68,7 @@ export class EditorPage extends BasePage { readonly addPictureButtonSelector = 'a >> text=Add Picture'; constructor(page: Page, readonly project: Project) { - super(page, `/app/lexicon/${project.id}/`, page.locator('.words-container-title')); + super(page, `/app/lexicon/${project.id}/`, page.locator('.words-container-title, .no-entries')); } async goto(options?: EditorGotoOptions): Promise { @@ -83,11 +84,16 @@ export class EditorPage extends BasePage { await expect(this.page.locator('.page-name >> text=' + this.project.name)).toBeVisible(); } - async navigateToSettings() { + async navigateToSettings(): Promise { await expect(this.settingsMenuLink).toBeVisible(); await this.settingsMenuLink.click(); await expect(this.projectSettingsLink).toBeVisible(); - await this.projectSettingsLink.click(); + const projectSettingsPage = new ProjectSettingsPage(this.page, this.project); + await Promise.all([ + this.projectSettingsLink.click(), + projectSettingsPage.waitForPage(), + ]); + return projectSettingsPage; } async navigateToEntriesList() { diff --git a/test/e2e/pages/project-settings.page.ts b/test/e2e/pages/project-settings.page.ts index a0967ce31e..92a14ff99f 100644 --- a/test/e2e/pages/project-settings.page.ts +++ b/test/e2e/pages/project-settings.page.ts @@ -9,7 +9,7 @@ export class ProjectSettingsPage extends BasePage { tabTitle: this.page.locator('text=Project Properties'), projectNameInput: this.page.locator('#projName'), defaultInterfaceLanguageInput: this.page.locator('#language'), - projectOwner: this.page.getByTestId('e2e-test-project-owner'), + projectOwner: this.page.locator('label:has-text("Project Owner") ~ div'), saveButton: this.page.locator('#project-settings-save-btn') }; readonly deleteTab = { @@ -23,9 +23,8 @@ export class ProjectSettingsPage extends BasePage { confirm: this.page.locator('div.modal-content >> text="Delete"') }; - constructor(page: Page, readonly project: Project) { - super(page, 'app/lexicon/' + project.id + '/#!/settings', page.locator('.page-name >> text=' + project.name)); + super(page, 'app/lexicon/' + project.id + '/#!/settings', page.locator('text=Project Properties')); } // navigate to project without UI @@ -45,12 +44,12 @@ export class ProjectSettingsPage extends BasePage { return await this.noticeList.count(); } - async setDefaultInterfaceLanguage(toLanguage: string, fromLanguage: string) { + async setDefaultInterfaceLanguage(language: string) { await expect(this.projectTab.tabTitle).toBeVisible(); - await expect(this.projectTab.saveButton).toBeVisible(); - await expect(this.projectTab.defaultInterfaceLanguageInput).toBeVisible(); - await expect(this.projectTab.defaultInterfaceLanguageInput.locator('option[selected="selected"]')).toHaveText(fromLanguage); - await this.projectTab.defaultInterfaceLanguageInput.selectOption({ label: toLanguage }); + await expect(this.projectTab.saveButton).toBeEnabled(); + // This selectOption() seems to sometimes have no effect. Potentially it has to do with the Angular state/lifecycle. + // The (or perhaps any) two awaits above seems to stabilise it quite well. + await this.projectTab.defaultInterfaceLanguageInput.selectOption({ label: language }); await this.projectTab.saveButton.click(); } } diff --git a/test/e2e/pages/user-profile.page.ts b/test/e2e/pages/user-profile.page.ts index 904e879b7b..84dc6fc925 100644 --- a/test/e2e/pages/user-profile.page.ts +++ b/test/e2e/pages/user-profile.page.ts @@ -8,7 +8,32 @@ export class UserProfilePage extends BasePage { myAccount: this.page.locator('#myAccountTab') }; + readonly accountTab = { + emailField: this.page.getByLabel('Email Address'), + usernameField: this.page.getByLabel('Username'), + colorField: this.page.locator('select:has-text("Select a Color...")'), + animalField: this.page.locator('select:has-text("Choose an animal...")'), + phoneField: this.page.getByLabel('Mobile Phone Number'), + updatesVia: { + email: this.page.locator('#EmailButton'), + sms: this.page.locator('#SMSButton'), + both: this.page.locator('#BothButton'), + } + }; + + readonly aboutMeTab = { + nameField: this.page.getByLabel('Full Name'), + ageField: this.page.getByLabel('Age'), + genderField: this.page.getByLabel('Gender'), + }; + + readonly saveBtn = this.page.locator('#saveBtn'); + + readonly modal = { + saveChangesBtn: this.page.locator('.modal-dialog button:has-text("Save changes")'), + }; + constructor(page: Page) { - super(page, '/app/userprofile', page.locator('.page-name >> text=Admin\'s User Profile')); + super(page, '/app/userprofile', page.locator('.page-name >> text=\'s User Profile')); } } diff --git a/playwright.config.ts b/test/e2e/playwright.config.ts similarity index 92% rename from playwright.config.ts rename to test/e2e/playwright.config.ts index 97cede9185..b0a5e0bd94 100644 --- a/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -1,5 +1,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; +import { expect } from '@playwright/test'; import { devices } from '@playwright/test'; +import { toHaveSelectedOption } from './utils/playwright-helpers'; /** * Read environment variables from file. @@ -7,11 +9,14 @@ import { devices } from '@playwright/test'; */ // require('dotenv').config(); +expect.extend({ + toHaveSelectedOption, +}); + /** * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { - testDir: './test/e2e', /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { @@ -24,7 +29,7 @@ const config: PlaywrightTestConfig = { /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Global setup for things like logging in users and saving login cookies */ - globalSetup: require.resolve('./test/e2e/utils/globalSetup'), + globalSetup: require.resolve('./utils/globalSetup'), /* Retry on CI only */ retries: process.env.CI ? 1 : 0, /* Opt out of parallel tests on CI. */ @@ -106,7 +111,7 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { - command: 'make playwright-app', + command: 'cd ../.. && make playwright-app', port: 3238, timeout: 15 * 1000, reuseExistingServer: true, diff --git a/test/e2e/project-settings.spec.ts b/test/e2e/project-settings.spec.ts index 67f0153a16..d046bdcabb 100644 --- a/test/e2e/project-settings.spec.ts +++ b/test/e2e/project-settings.spec.ts @@ -31,7 +31,8 @@ test.describe('E2E Project Settings app', () => { id: '' }; - test.beforeAll(async ({ request, admin, member, manager }) => {for (const project of projects) { + test.beforeAll(async ({ request, admin, member, manager }) => { + for (const project of projects) { const projectId = await initTestProject(request, project.code, project.name, admin.username, [member.username]); project.id = projectId; } @@ -46,6 +47,13 @@ test.describe('E2E Project Settings app', () => { await expect(editorPage.settingsMenuLink).not.toBeVisible(); }); + test('Project member can navigate from editor to settings', async({ managerTab }) => { + const editorPage = new EditorPage(managerTab, projects[0]); + await editorPage.goto(); + const projectSettingsPage = await editorPage.navigateToSettings(); + await expect(projectSettingsPage.projectTab.tabTitle).toBeVisible(); + }); + test('Project owner can manage project they own', async ({ adminTab }) => { const projectSettingsPage = new ProjectSettingsPage(adminTab, projects[0]); await projectSettingsPage.goto(); diff --git a/test/e2e/semantic-domains.spec.ts b/test/e2e/semantic-domains.spec.ts index 3ad0bf7563..4d981f5ef3 100644 --- a/test/e2e/semantic-domains.spec.ts +++ b/test/e2e/semantic-domains.spec.ts @@ -12,7 +12,6 @@ import constants from './testConstants.json'; import { EditorPage } from './pages/editor.page'; import { PageHeader } from './components/page-header.component'; import { ProjectSettingsPage } from './pages/project-settings.page'; -import { expectOptionSelectedInSelectElement } from './utils/playwright-helpers'; test.describe('Lexicon E2E Semantic Domains Lazy Load', () => { let projectsPageManager: ProjectsPage; @@ -48,18 +47,21 @@ test.describe('Lexicon E2E Semantic Domains Lazy Load', () => { // can change Project default language to Thai await projectSettingsPage.goto(); - await projectSettingsPage.setDefaultInterfaceLanguage('ภาษาไทย - semantic domain only', 'English'); - await expectOptionSelectedInSelectElement(projectSettingsPage.projectTab.defaultInterfaceLanguageInput, 'ภาษาไทย'); + await projectSettingsPage.setDefaultInterfaceLanguage('ภาษาไทย - semantic domain only'); + await expect(projectSettingsPage.projectTab.defaultInterfaceLanguageInput) + .toHaveSelectedOption({label: 'ภาษาไทย - semantic domain only'}); await expect(pageHeader.languageDropdownButton).toHaveText('ภาษาไทย'); // should be using Thai semantic domain await editorPage.goto(); - await expect(editorPage.senseCard.locator(editorPage.semanticDomainSelector).first()).toHaveText(semanticDomain1dot1Thai); + await expect(editorPage.senseCard.locator(editorPage.semanticDomainSelector).first()) + .toHaveText(semanticDomain1dot1Thai); // can change Project default language back to English await projectSettingsPage.goto(); - await projectSettingsPage.setDefaultInterfaceLanguage('English', 'ภาษาไทย - semantic domain only'); - await expectOptionSelectedInSelectElement(projectSettingsPage.projectTab.defaultInterfaceLanguageInput, 'English'); + await projectSettingsPage.setDefaultInterfaceLanguage('English'); + await expect(projectSettingsPage.projectTab.defaultInterfaceLanguageInput) + .toHaveSelectedOption({label: 'English'}); await expect(pageHeader.languageDropdownButton).toHaveText('English'); // should be using English Semantic Domain @@ -68,12 +70,14 @@ test.describe('Lexicon E2E Semantic Domains Lazy Load', () => { // can change Project default language back to Thai await projectSettingsPage.goto(); - await projectSettingsPage.setDefaultInterfaceLanguage('ภาษาไทย - semantic domain only', 'English'); - await expectOptionSelectedInSelectElement(projectSettingsPage.projectTab.defaultInterfaceLanguageInput, 'ภาษาไทย'); + await projectSettingsPage.setDefaultInterfaceLanguage('ภาษาไทย - semantic domain only'); + await expect(projectSettingsPage.projectTab.defaultInterfaceLanguageInput) + .toHaveSelectedOption({label: 'ภาษาไทย - semantic domain only'}); // should be using Thai Semantic Domain await editorPage.goto(); - await expect(editorPage.senseCard.locator(editorPage.semanticDomainSelector).first()).toHaveText(semanticDomain1dot1Thai); + await expect(editorPage.senseCard.locator(editorPage.semanticDomainSelector).first()) + .toHaveText(semanticDomain1dot1Thai); // can change user interface language await expect(pageHeader.languageDropdownButton).toHaveText('ภาษาไทย'); @@ -82,11 +86,13 @@ test.describe('Lexicon E2E Semantic Domains Lazy Load', () => { await expect(pageHeader.languageDropdownButton).toHaveText('English'); // should be using English Semantic Domain - await expect(editorPage.senseCard.locator(editorPage.semanticDomainSelector).first()).toHaveText(semanticDomain1dot1English); + await expect(editorPage.senseCard.locator(editorPage.semanticDomainSelector).first()) + .toHaveText(semanticDomain1dot1English); // should still have Thai for Project default language await projectSettingsPage.goto(); - await expect(projectSettingsPage.projectTab.defaultInterfaceLanguageInput.locator('option[selected="selected"]')).toHaveText('ภาษาไทย - semantic domain only'); + await expect(projectSettingsPage.projectTab.defaultInterfaceLanguageInput) + .toHaveSelectedOption({label: 'ภาษาไทย - semantic domain only'}); // user interface language should still be English await expect(pageHeader.languageDropdownButton).toHaveText('English'); diff --git a/test/e2e/types/index.d.ts b/test/e2e/types/index.d.ts new file mode 100644 index 0000000000..0f32c6939c --- /dev/null +++ b/test/e2e/types/index.d.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + namespace PlaywrightTest { + interface Matchers { + toHaveSelectedOption(option: {label?: string, value?: string}): Promise; + } + } +} diff --git a/test/e2e/user-profile.spec.ts b/test/e2e/user-profile.spec.ts new file mode 100644 index 0000000000..a0b0cd1c7a --- /dev/null +++ b/test/e2e/user-profile.spec.ts @@ -0,0 +1,113 @@ +import { test, UserTab, UserDetails } from './utils/fixtures'; +import { UserProfilePage } from './pages/user-profile.page'; +import { expect } from '@playwright/test'; +import { LoginPage } from './pages/login.page'; +import { ProjectsPage } from './pages/projects.page'; + +test.describe('E2E User Profile', () => { + + /* + Ultimately the "writable" user should get reset each time to prevent it from getting stuck in an invalid state. + I would expect `initUser` to do that, but it doesn't. + If this test ever gives us trouble as a result then + it would be worth looking into getting this to work properly. + + test.beforeEach(async ({context}) => { + await initUser(context, 'writable'); + }); + */ + + test('Generated user account and about me info', async ({member2Tab}) => { + const userProfilePage = new UserProfilePage(member2Tab); + await userProfilePage.goto(); + + await expect(userProfilePage.accountTab.emailField).toHaveValue(member2Tab.email); + await expect(userProfilePage.accountTab.usernameField).toHaveValue(member2Tab.username); + await expect(userProfilePage.accountTab.phoneField).toHaveValue(''); + await expect(userProfilePage.accountTab.updatesVia.email).toHaveClass(/active/); + + await userProfilePage.tabs.aboutMe.click(); + + await expect(userProfilePage.aboutMeTab.nameField).toHaveValue(member2Tab.name); + await expect(userProfilePage.aboutMeTab.ageField).toHaveValue(''); + }); + + + test('Update user account info', async ({writableTab}) => { + const userProfilePage = new UserProfilePage(writableTab); + await userProfilePage.goto(); + + const newEmail = `newemail-${Date.now()}@example.com`; + await userProfilePage.accountTab.emailField.fill(newEmail); + await userProfilePage.accountTab.colorField.selectOption({label: 'Steel Blue'}); + await userProfilePage.accountTab.animalField.selectOption({label: 'Otter'}); + const newPhone = `+1876 ${Date.now().toString().slice(0, 7)}`; + await userProfilePage.accountTab.phoneField.fill(newPhone); + await userProfilePage.accountTab.updatesVia.both.click(); + + await userProfilePage.saveBtn.click(); + await Promise.all([ + userProfilePage.page.reload(), + userProfilePage.waitForPage(), + ]); + + await expect(userProfilePage.accountTab.emailField).toHaveValue(newEmail); + await expect(userProfilePage.accountTab.colorField).toHaveSelectedOption({label: 'Steel Blue'}); + await expect(userProfilePage.accountTab.animalField).toHaveSelectedOption({label: 'Otter'}); + await expect(userProfilePage.accountTab.phoneField).toHaveValue(newPhone); + await expect(userProfilePage.accountTab.updatesVia.both).toHaveClass(/active/); + }); + + test('Update username and re-login', async ({writableTab}) => { + const currUsername = writableTab.username; + const newUsername = `${writableTab.username}-new`; + + await changeUsernameAndLogin(newUsername, writableTab, writableTab); + const newDetails = {...writableTab, username: newUsername}; + await changeUsernameAndLogin(currUsername, newDetails, writableTab); + }); + + const changeUsernameAndLogin = async (newUsername: string, currDetails: UserDetails, tab: UserTab): Promise => { + const userProfilePage = new UserProfilePage(tab); + await userProfilePage.goto(); + + await expect(userProfilePage.accountTab.usernameField).toHaveValue(currDetails.username); + await userProfilePage.accountTab.usernameField.fill(newUsername); + await userProfilePage.saveBtn.click(); + + const loginPage = new LoginPage(tab); + + await Promise.all([ + userProfilePage.modal.saveChangesBtn.click(), + loginPage.waitForPage(), + ]); + + await Promise.all([ + loginPage.loginAs(newUsername, currDetails.password), + new ProjectsPage(tab).waitForPage(), + ]); + }; + + test('Update user about me info', async ({writableTab}) => { + const userProfilePage = new UserProfilePage(writableTab); + await userProfilePage.goto(); + await userProfilePage.tabs.aboutMe.click(); + + const newName = `Name - ${Date.now()}`; + await userProfilePage.aboutMeTab.nameField.fill(newName); + const newAge = `${~~(Math.random() * 30) + 20}`; // random between 20 - 50 + await userProfilePage.aboutMeTab.ageField.fill(newAge); + await userProfilePage.aboutMeTab.genderField.selectOption({label: 'Female'}); + + await userProfilePage.saveBtn.click(); + await Promise.all([ + userProfilePage.page.reload(), + userProfilePage.waitForPage(), + ]); + + await expect(userProfilePage.aboutMeTab.nameField).toHaveValue(newName); + await expect(userProfilePage.aboutMeTab.ageField).toHaveValue(newAge); + await expect(userProfilePage.aboutMeTab.genderField).toHaveSelectedOption({label:'Female'}); + }); + +}); diff --git a/test/e2e/utils/e2e-users.ts b/test/e2e/utils/e2e-users.ts new file mode 100644 index 0000000000..4c6d4dca88 --- /dev/null +++ b/test/e2e/utils/e2e-users.ts @@ -0,0 +1,10 @@ +export const usersToCreate = [ + 'admin', + 'manager', + 'member', + 'member2', + 'observer', + 'writable' +] as const; + +export type E2EUsernames = typeof usersToCreate[number]; diff --git a/test/e2e/utils/fixtures.ts b/test/e2e/utils/fixtures.ts index 8032bd5ab4..d0a948362b 100644 --- a/test/e2e/utils/fixtures.ts +++ b/test/e2e/utils/fixtures.ts @@ -1,7 +1,7 @@ // example.spec.ts import { test as base } from '@playwright/test'; import type { Browser, Page } from '@playwright/test'; -import type { usernamesForFixture } from './userFixtures'; +import type { E2EUsernames } from './e2e-users'; import constants from '../testConstants.json'; export type UserDetails = { @@ -13,14 +13,14 @@ export type UserDetails = { export type UserTab = Page & UserDetails; -function setupUserDetails(obj: any, username: usernamesForFixture) { +function setupUserDetails(obj: UserDetails, username: E2EUsernames) { obj.username = constants[`${username}Username`] ?? username; obj.name = constants[`${username}Name`] ?? username; obj.password = constants[`${username}Password`] ?? 'x'; obj.email = constants[`${username}Email`] ?? `${username}@example.com`; } -const userTab = (username: usernamesForFixture) => async ({ browser, browserName }: { browser: Browser, browserName: string}, use: (r: UserTab) => Promise) => { +const userTab = (username: E2EUsernames) => async ({ browser, browserName }: { browser: Browser, browserName: string}, use: (r: UserTab) => Promise) => { const storageState = `${browserName}-${username}-storageState.json`; const context = await browser.newContext({ storageState }) const page = await context.newPage(); @@ -29,6 +29,12 @@ const userTab = (username: usernamesForFixture) => async ({ browser, browserName await use(tab); } +const userDetails = (username: E2EUsernames) => async ({}, use: (r: UserDetails) => Promise) => { + let user = {} as UserDetails; + setupUserDetails(user, username); + await use(user); +}; + // Add user fixtures to test function // Two kinds of fixtures: userTab and user, where "user" is one of "admin", "manager", "member", "member2", or "observer" // The userTab fixture represents a browser tab (a "page" in Playwright terms) that's already logged in as that user @@ -42,48 +48,32 @@ export const test = (base memberTab: UserTab, member2Tab: UserTab, observerTab: UserTab, + writableTab: UserTab, anonTab: Page, admin: UserDetails, manager: UserDetails, member: UserDetails, member2: UserDetails, observer: UserDetails, + writable: UserDetails, }>({ adminTab: userTab('admin'), managerTab: userTab('manager'), memberTab: userTab('member'), member2Tab: userTab('member2'), observerTab: userTab('observer'), + writableTab: userTab('writable'), anonTab: async ({ browser }: { browser: Browser }, use: (r: Page) => Promise) => { const context = await browser.newContext(); const page = await context.newPage(); const tab = page; await use(tab); }, - admin: async ({}, use) => { - let admin = {} as UserDetails; - setupUserDetails(admin, 'admin'); - await use(admin); - }, - manager: async ({}, use) => { - let manager = {} as UserDetails; - setupUserDetails(manager, 'manager'); - await use(manager); - }, - member: async ({}, use) => { - let member = {} as UserDetails; - setupUserDetails(member, 'member'); - await use(member); - }, - member2: async ({}, use) => { - let member2 = {} as UserDetails; - setupUserDetails(member2, 'member2'); - await use(member2); - }, - observer: async ({}, use) => { - let observer = {} as UserDetails; - setupUserDetails(observer, 'observer'); - await use(observer); - } + admin: userDetails('admin'), + manager: userDetails('manager'), + member: userDetails('member'), + member2: userDetails('member2'), + observer: userDetails('observer'), + writable: userDetails('writable'), }) ); diff --git a/test/e2e/utils/globalSetup.ts b/test/e2e/utils/globalSetup.ts index c8070422fa..3d2fd32e97 100644 --- a/test/e2e/utils/globalSetup.ts +++ b/test/e2e/utils/globalSetup.ts @@ -1,19 +1,8 @@ // playwright.config.ts -import type { APIRequestContext, FullConfig } from '@playwright/test'; +import { FullConfig } from '@playwright/test'; import { chromium, firefox, webkit } from '@playwright/test'; -import { testControl } from './jsonrpc'; -import constants from '../testConstants.json'; -import { loginAs } from './login'; -import { usersToCreate } from './userFixtures'; -import * as fs from 'fs'; - -function createUser(request: APIRequestContext, baseName: string) { - const username = constants[`${baseName}Username`] ?? baseName; - const fullName = constants[`${baseName}Name`] ?? username; - const password = constants[`${baseName}Password`] ?? 'x'; - const email = constants[`${baseName}Email`] ?? `${username}@example.com`; - return testControl(request, 'create_user', [username, fullName, password, email]); -} +import { initUser } from './user-tools'; +import { usersToCreate } from './e2e-users'; export default async function globalSetup(config: FullConfig) { try { @@ -31,26 +20,9 @@ export default async function globalSetup(config: FullConfig) { chromium ); const browser = await projectBrowser.launch(); - const context = await browser.newContext({ baseURL }); - for (const user of usersToCreate) { - await createUser(context.request, user); - } - await context.close(); - - // Now log in as each user and ensure there's a storage state saved - const sessionLifetime = 365 * 24 * 60 * 60 * 1000; // 1 year, in milliseconds - const now = new Date(); - const sessionCutoff = now.getTime() - sessionLifetime; for (const user of usersToCreate) { - const path = `${browserName}-${user}-storageState.json`; - if (fs.existsSync(path) && fs.statSync(path)?.ctimeMs >= sessionCutoff) { - // Storage state file is recent, no need to re-create it - continue; - } const context = await browser.newContext({ baseURL }); - const page = await context.newPage(); - await loginAs(page, user); - await context.storageState({ path }); + await initUser(context, user); await context.close(); } } diff --git a/test/e2e/utils/login.ts b/test/e2e/utils/login.ts index 36a64d3047..222b13a81f 100644 --- a/test/e2e/utils/login.ts +++ b/test/e2e/utils/login.ts @@ -1,6 +1,6 @@ import { Browser, Page } from '@playwright/test'; import constants from '../testConstants.json'; -import type { usernamesForFixture } from './userFixtures'; +import type { E2EUsernames } from './e2e-users'; export async function login(page: Page, username: string, password: string) { await page.goto('/auth/login'); @@ -16,7 +16,7 @@ export async function logout(page: Page) { return await page.goto('/auth/logout'); } -export function getLoginInfo(name: usernamesForFixture) { +export function getLoginInfo(name: E2EUsernames) { const usernameKey = `${name}Username`; const passwordKey = `${name}Password`; if (Object.hasOwnProperty.call(constants, usernameKey)) { @@ -29,7 +29,7 @@ export function getLoginInfo(name: usernamesForFixture) { } } -export function loginAs(page: Page, name: usernamesForFixture) { +export function loginAs(page: Page, name: E2EUsernames) { const { username, password } = getLoginInfo(name); return login(page, username, password); } diff --git a/test/e2e/utils/playwright-helpers.ts b/test/e2e/utils/playwright-helpers.ts index 88071aa570..b5cb3f364e 100644 --- a/test/e2e/utils/playwright-helpers.ts +++ b/test/e2e/utils/playwright-helpers.ts @@ -1,14 +1,30 @@ -import { expect, Locator, Page } from '@playwright/test'; - -/** - * Check whether the option that should be selected is selected. - * Playwright does not hava a way to directly get the text of the selected option. - * This function therefore works with the value. - * @param selectElement , \ - * @param expectedOptionText , e.g. 'Pizza Margherita' or a substring, e.g. 'Margherita' - */ -export async function expectOptionSelectedInSelectElement(selectElement: Locator, expectedOptionText: string) { - const expectedOption = selectElement.locator('option').filter({ hasText: expectedOptionText }); - const expectedValue = await expectedOption.getAttribute('value'); - await expect(selectElement).toHaveValue(expectedValue); -} +import { expect, Locator } from '@playwright/test'; + +export const toHaveSelectedOption = async (select: Locator, option: {label?: string, value?: string}) => { + if (option.label === undefined && option.value === undefined) { + throw new Error('At least one of either label or value must be set'); + } + + const value = await select.inputValue(); + + if (option.value !== undefined) { + await expect(select).toHaveValue(option.value); + } + + if (option.label !== undefined) { + const optionElem = select.locator(`option[value="${value}"]`); + const optionLabel = await optionElem.textContent(); + + if (option.label === optionLabel) { + return { + message: () => `Did not expect '${option.label}' to be selected.`, + pass: true, + }; + } else { + return { + message: () => `Expected '${option.label}' to be selected, but was '${optionLabel}'.`, + pass: false, + }; + } + } +}; diff --git a/test/e2e/utils/user-tools.ts b/test/e2e/utils/user-tools.ts new file mode 100644 index 0000000000..29e136d86d --- /dev/null +++ b/test/e2e/utils/user-tools.ts @@ -0,0 +1,32 @@ +import { APIRequestContext, BrowserContext } from "@playwright/test"; +import { testControl } from "./jsonrpc"; +import { loginAs } from "./login"; +import { E2EUsernames } from "./e2e-users"; +import * as fs from 'fs'; +import constants from "../testConstants.json"; + +const SESSION_LIFETIME = 365 * 24 * 60 * 60 * 1000; // 1 year, in milliseconds + +function createUser(request: APIRequestContext, baseName: string) { + const username = constants[`${baseName}Username`] ?? baseName; + const fullName = constants[`${baseName}Name`] ?? username; + const password = constants[`${baseName}Password`] ?? 'x'; + const email = constants[`${baseName}Email`] ?? `${username}@example.com`; + return testControl(request, 'create_user', [username, fullName, password, email]); +} + +export async function initUser(context: BrowserContext, user: E2EUsernames) { + const id = await createUser(context.request, user); + + // Now log in and ensure there's a storage state saved + const sessionCutoff = Date.now() - SESSION_LIFETIME; + const browserName = context.browser().browserType().name(); + const path = `${browserName}-${user}-storageState.json`; + if (fs.existsSync(path) && fs.statSync(path)?.ctimeMs >= sessionCutoff) { + // Storage state file is recent, no need to re-create it + return; + } + const page = await context.newPage(); + await loginAs(page, user); + await context.storageState({ path }); +} diff --git a/test/e2e/utils/userFixtures.ts b/test/e2e/utils/userFixtures.ts deleted file mode 100644 index 95c2ab9a29..0000000000 --- a/test/e2e/utils/userFixtures.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type usernamesForFixture = - 'admin' | - 'manager' | - 'member' | - 'member2' | - 'observer' -; - -export const usersToCreate: usernamesForFixture[] = [ - 'admin', - 'manager', - 'member', - 'member2', - 'observer', -]; From 1f44371249ac70131b95a36dd5ab26cb615513ce Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 17 Nov 2022 20:45:26 +0700 Subject: [PATCH 05/10] add .dockerignore to prevent vendor files from getting included when they shouldn't be. (#1599) --- .dockerignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..86f25a1db8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# The .dockerignore file excludes files from the container build process. +# +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +# Exclude locally vendored dependencies. +src/vendor/ + +# Exclude "build-time" ignore files. +.dockerignore + +# Exclude git history and configuration. +.gitignore +.git From 01ee2d4fa081bb468c1feb06e09e4ba43ff30b25 Mon Sep 17 00:00:00 2001 From: Christopher Hirt Date: Thu, 17 Nov 2022 22:32:51 +0700 Subject: [PATCH 06/10] adjust audio input system regex to be more inclusive (#1604) Per a user's report, their audio input system was not properly detected in LF due to a too-strict regex. This loosens the regex a bit to allow LF to properly identify the field as an audio input system. With this change, a tag must have "-Zxxx" and end with "audio" Before this change, a non-standard input system tag like lwl-Zxxx-x-minority-audio is not detected as an audio input system (although FLEx handles it fine). After this change, this input system tag should work. --- src/angular-app/bellows/core/utility.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular-app/bellows/core/utility.service.ts b/src/angular-app/bellows/core/utility.service.ts index b8071f24c0..f54eaed002 100644 --- a/src/angular-app/bellows/core/utility.service.ts +++ b/src/angular-app/bellows/core/utility.service.ts @@ -24,7 +24,7 @@ export class UtilityService { } static isAudio(tag: string): boolean { - const tagAudioPattern = /^\w{2,3}-Zxxx-x(-\w{2,3})*-[aA][uU][dD][iI][oO]$/; + const tagAudioPattern = /^\w{2,3}-Zxxx-x-.*[aA][uU][dD][iI][oO]$/; return tagAudioPattern.test(tag); } From daeda8d5cf07ec420e191448b00fd447890d619c Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 17 Nov 2022 22:34:28 +0700 Subject: [PATCH 07/10] Fix moving playwright.config.ts (#1603) * Fix test-results path in github action * Standardize path/handling of storageState.json files --- .github/workflows/pull-request.yml | 2 +- .gitignore | 4 ++-- Makefile | 7 +++++-- test/e2e/semantic-domains.spec.ts | 10 +--------- test/e2e/utils/fixtures.ts | 3 ++- test/e2e/utils/login.ts | 7 +++++-- test/e2e/utils/user-tools.ts | 13 ++++++++++++- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f5765bd670..1ad47a033f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -67,7 +67,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: test-results - path: test-results + path: test/e2e/test-results check-code-formatting: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 6445ffc77d..60502aa4fc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,6 @@ test/php/\.phpunit\.result\.cache node_modules .DS_Store docker-scan-results.txt -test-results/ -playwright-report/ +test-results +test-storage-state *storageState.json diff --git a/Makefile b/Makefile index eacebbde7b..f508db34dd 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ playwright-tests: .PHONY: playwright-app playwright-app: # delete any cached session storage state files if the service isn't running - docker compose ps app-for-playwright > /dev/null 2>&1 || rm -f *-storageState.json + docker compose ps app-for-playwright > /dev/null 2>&1 || $(MAKE) clean-test docker compose up -d app-for-playwright # wait until the app-for-playwright service is serving up HTTP before continuing until curl localhost:3238 > /dev/null 2>&1; do sleep 1; done @@ -83,9 +83,12 @@ clean: docker compose down docker system prune -f +clean-test: + cd test/e2e && npx rimraf test-storage-state test-results + .PHONY: clean-powerwash clean-powerwash: clean - npx rimraf *storageState.json test-results + $(MAKE) clean-test docker system prune -f --volumes - docker rmi -f `docker images -q "lf-*"` sillsdev/web-languageforge:base-php docker builder prune -f diff --git a/test/e2e/semantic-domains.spec.ts b/test/e2e/semantic-domains.spec.ts index 4d981f5ef3..a719284fc9 100644 --- a/test/e2e/semantic-domains.spec.ts +++ b/test/e2e/semantic-domains.spec.ts @@ -1,20 +1,13 @@ import { expect } from '@playwright/test'; import { test } from './utils/fixtures'; - -import { ProjectsPage } from './pages/projects.page'; - import { Project } from './utils/types'; - import { addLexEntry, addPictureFileToProject, initTestProject } from './utils/testSetup'; - - import constants from './testConstants.json'; import { EditorPage } from './pages/editor.page'; import { PageHeader } from './components/page-header.component'; import { ProjectSettingsPage } from './pages/project-settings.page'; test.describe('Lexicon E2E Semantic Domains Lazy Load', () => { - let projectsPageManager: ProjectsPage; let editorPage: EditorPage; let pageHeader: PageHeader; const project: Project = { @@ -26,11 +19,10 @@ test.describe('Lexicon E2E Semantic Domains Lazy Load', () => { const semanticDomain1dot1English = constants.testEntry1.senses[0].semanticDomain.values[0] + ' Sky'; const semanticDomain1dot1Thai = constants.testEntry1.senses[0].semanticDomain.values[0] + ' ท้องฟ้า'; - test.beforeAll(async ({ request, managerTab, member, manager, admin, }) => { + test.beforeAll(async ({ request, managerTab, manager, admin, }) => { project.id = await initTestProject(request, project.code, project.name, manager.username, [admin.username]); await addPictureFileToProject(request, project.code, constants.testEntry1.senses[0].pictures[0].fileName); await addLexEntry(request, project.code, constants.testEntry1); - projectsPageManager = new ProjectsPage(managerTab); editorPage = new EditorPage(managerTab, project); pageHeader = new PageHeader(editorPage.page); }); diff --git a/test/e2e/utils/fixtures.ts b/test/e2e/utils/fixtures.ts index d0a948362b..67ff0ca4e7 100644 --- a/test/e2e/utils/fixtures.ts +++ b/test/e2e/utils/fixtures.ts @@ -3,6 +3,7 @@ import { test as base } from '@playwright/test'; import type { Browser, Page } from '@playwright/test'; import type { E2EUsernames } from './e2e-users'; import constants from '../testConstants.json'; +import { getStorageStatePath } from './user-tools'; export type UserDetails = { username: string, @@ -21,7 +22,7 @@ function setupUserDetails(obj: UserDetails, username: E2EUsernames) { } const userTab = (username: E2EUsernames) => async ({ browser, browserName }: { browser: Browser, browserName: string}, use: (r: UserTab) => Promise) => { - const storageState = `${browserName}-${username}-storageState.json`; + const storageState = getStorageStatePath(browserName, username); const context = await browser.newContext({ storageState }) const page = await context.newPage(); const tab = page as UserTab; diff --git a/test/e2e/utils/login.ts b/test/e2e/utils/login.ts index 222b13a81f..576cc84463 100644 --- a/test/e2e/utils/login.ts +++ b/test/e2e/utils/login.ts @@ -1,6 +1,7 @@ import { Browser, Page } from '@playwright/test'; import constants from '../testConstants.json'; import type { E2EUsernames } from './e2e-users'; +import { getStorageStatePath } from './user-tools'; export async function login(page: Page, username: string, password: string) { await page.goto('/auth/login'); @@ -34,7 +35,9 @@ export function loginAs(page: Page, name: E2EUsernames) { return login(page, username, password); } -export async function getLoggedInPage(browser: Browser, name: string) { - const context = await browser.newContext({ storageState: `${name}-storageState.json` }); +export async function getLoggedInPage(browser: Browser, user: string) { + const browserName = browser.browserType().name(); + const storageState = getStorageStatePath(browserName, user); + const context = await browser.newContext({ storageState }); return await context.newPage(); } diff --git a/test/e2e/utils/user-tools.ts b/test/e2e/utils/user-tools.ts index 29e136d86d..0ab3254dac 100644 --- a/test/e2e/utils/user-tools.ts +++ b/test/e2e/utils/user-tools.ts @@ -4,6 +4,7 @@ import { loginAs } from "./login"; import { E2EUsernames } from "./e2e-users"; import * as fs from 'fs'; import constants from "../testConstants.json"; +import path from "path"; const SESSION_LIFETIME = 365 * 24 * 60 * 60 * 1000; // 1 year, in milliseconds @@ -21,7 +22,7 @@ export async function initUser(context: BrowserContext, user: E2EUsernames) { // Now log in and ensure there's a storage state saved const sessionCutoff = Date.now() - SESSION_LIFETIME; const browserName = context.browser().browserType().name(); - const path = `${browserName}-${user}-storageState.json`; + const path = getStorageStatePath(browserName, user); if (fs.existsSync(path) && fs.statSync(path)?.ctimeMs >= sessionCutoff) { // Storage state file is recent, no need to re-create it return; @@ -30,3 +31,13 @@ export async function initUser(context: BrowserContext, user: E2EUsernames) { await loginAs(page, user); await context.storageState({ path }); } + +export function getStorageStatePath(browser: string, user: string): string { + const testRoot = 'test/e2e'; + const storageRoot = 'test-storage-state' + const storageState = `${browser}-${user}-storageState.json`; + // true if running tests with VS-Code extension otherwise false + return process.cwd().endsWith(testRoot) + ? path.join(storageRoot, storageState) + : path.join(testRoot, storageRoot, storageState); +} From dc30065293ccdd1bf4d0124b63acd9dbd40b68e2 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 17 Nov 2022 22:46:40 +0700 Subject: [PATCH 08/10] Enable playwright tests in deployment workflow (#1558) Playwright tests seem to be pretty stable now, so we can reenable them in the deployment workflow/job. I removed the browser caching steps with the thought that deployment doesn't run often enough for them to add as much value as they do complexity. --- .github/workflows/integrate-and-deploy.yml | 16 ++++++++++------ .github/workflows/pull-request.yml | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integrate-and-deploy.yml b/.github/workflows/integrate-and-deploy.yml index 440976506e..3ce082b79c 100644 --- a/.github/workflows/integrate-and-deploy.yml +++ b/.github/workflows/integrate-and-deploy.yml @@ -74,12 +74,16 @@ jobs: - name: Unit Tests run: make unit-tests-ci - # - - # name: E2E Tests - # run: | - # docker compose -f docker-compose.yml up -d app-for-playwright - # npx playwright install chromium - # npx playwright test + - + name: Playwright E2E Tests + run: make playwright-tests-ci + - + name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-results + path: test-results - name: Log in to Docker Hub uses: docker/login-action@v2 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1ad47a033f..e98c13bcaa 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -62,7 +62,7 @@ jobs: run: make playwright-tests-ci - - name: Upload test results + name: Upload Playwright test results if: always() uses: actions/upload-artifact@v3 with: From d244653f2664c774da9544910c467fd14cecbb86 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 18 Nov 2022 00:35:17 +0700 Subject: [PATCH 09/10] Clarify use of title/description and add auto-merge reminder (#1602) --- .github/pull_request_template.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 30a7e4ff55..7ea2ff1f59 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,9 +22,10 @@ Please provide screenshots / animations for any change that involves the UI. Ple ## Checklist - [ ] I have performed a self-review of my own code -- [ ] I have reviewed the title/description of this PR which will be used as the squashed PR commit message +- [ ] I have reviewed the title & description of this PR which I will use as the squashed PR commit message - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have enabled auto-merge (optional) ## How to test From 4cc056ae3da48b526a486ee3079112e760b483c3 Mon Sep 17 00:00:00 2001 From: laineyhm <56163492+laineyhm@users.noreply.github.com> Date: Fri, 18 Nov 2022 14:27:17 +0700 Subject: [PATCH 10/10] make empty projects display correctly for non-managers (#1606) --- .../languageforge/lexicon/editor/editor-list.view.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular-app/languageforge/lexicon/editor/editor-list.view.html b/src/angular-app/languageforge/lexicon/editor/editor-list.view.html index e079db03a7..b4ba963515 100644 --- a/src/angular-app/languageforge/lexicon/editor/editor-list.view.html +++ b/src/angular-app/languageforge/lexicon/editor/editor-list.view.html @@ -109,12 +109,12 @@
-
+

Looks like there are no entries yet.

-