From d81d4adf63310a59b6d7fa4bb04fb80fc91821ce Mon Sep 17 00:00:00 2001 From: Ben Polinsky Date: Tue, 17 Dec 2024 12:01:16 -0500 Subject: [PATCH 1/5] add electron integration test fixtures --- .../integration-test/fixtures/AuthFixture.ts | 46 +++++++ .../fixtures/ElectronAppFixture.ts | 63 +++++++++ .../fixtures/ElectronFixtures.ts | 33 +++++ .../integration-test/helpers/TestHelper.ts | 61 --------- .../src/integration-test/helpers/utils.ts | 28 ++++ .../src/integration-test/integration.test.ts | 129 ++++-------------- .../electron/src/integration-test/types.ts | 14 -- 7 files changed, 193 insertions(+), 181 deletions(-) create mode 100644 packages/electron/src/integration-test/fixtures/AuthFixture.ts create mode 100644 packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts create mode 100644 packages/electron/src/integration-test/fixtures/ElectronFixtures.ts delete mode 100644 packages/electron/src/integration-test/helpers/TestHelper.ts create mode 100644 packages/electron/src/integration-test/helpers/utils.ts delete mode 100644 packages/electron/src/integration-test/types.ts diff --git a/packages/electron/src/integration-test/fixtures/AuthFixture.ts b/packages/electron/src/integration-test/fixtures/AuthFixture.ts new file mode 100644 index 00000000..31e48ce1 --- /dev/null +++ b/packages/electron/src/integration-test/fixtures/AuthFixture.ts @@ -0,0 +1,46 @@ +import type { Page } from "@playwright/test"; +import { RefreshTokenStore } from "../../main/TokenStore"; + +interface AuthFixtureProps { + page: Page; + tokenStore: RefreshTokenStore +} + +export class AuthFixture { + private _page: Page; + private _tokenStore: RefreshTokenStore + + constructor(options: AuthFixtureProps) { + this._page = options.page; + this._tokenStore = options.tokenStore; + } + + public async signInIMS(url: string, email: string, password: string) { + await this._page.goto(url); + await this._page.waitForSelector("#identifierInput", { timeout: 5000 }); + await this._page.getByLabel("Email address").fill(email); + await this._page.getByLabel("Email address").press("Enter"); + await this._page.getByLabel("Password").fill(password); + await this._page.getByText("Sign In").click(); + + const consentUrl = this._page.url(); + if (consentUrl.endsWith("resume/as/authorization.ping")) { + await this.handleConsentScreen(); + } + } + + // Admittedly this is cheating: no user would interact + // with the tokenStore directly, but this is a tough + // case to test otherwise. + public async switchScopes(scopes: string) { + await this._tokenStore.load(scopes); + } + + private async handleConsentScreen() { + const consentAcceptButton = this._page.getByRole("link", { + name: "Accept", + }); + if (consentAcceptButton) + await consentAcceptButton.click(); + } +} diff --git a/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts b/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts new file mode 100644 index 00000000..2832ccf5 --- /dev/null +++ b/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts @@ -0,0 +1,63 @@ +import { ElectronApplication, Page } from "@playwright/test"; + +export class ElectronAppFixture { + constructor(private _page: Page, private _app: ElectronApplication) { } + + get buttons() { + return { + signIn: this._page.getByTestId("signIn"), + signOut: this._page.getByTestId("signOut"), + status: this._page.getByTestId("getStatus") + } + } + + /** + * Signs in, captures the URL electron opens internally, and returns for IMS. + */ + public async clickSignIn(): Promise { + await this._page.waitForSelector("button#signIn"); + const button = this._page.getByTestId("signIn"); + const clickPromise = button.click(); + const urlPromise = this.getUrl(); + const [, url] = await Promise.all([clickPromise, urlPromise]); + return url + } + + public async clickSignOut() { + await this._page.waitForSelector("button#signOut"); + const button = this._page.getByTestId("signOut"); + await button.click(); + } + + public async isSignedIn() { + const button = this._page.getByTestId("getStatus"); + await button.click(); + const locator = this._page.getByText("signed in"); + return locator.isVisible(); + } + + public async checkStatus(expectedStatus: boolean) { + await this._page.waitForSelector("button#getStatus"); + const button = this._page.getByTestId("getStatus"); + await button.click(); + this._page.getByText(expectedStatus ? "signed in" : "signed out"); + } + + /** + * Captures the URL that electron opens internally using its shell module. + */ + public async getUrl(): Promise { + return this._app.evaluate(async ({ shell }) => { + return new Promise((resolve) => { + shell.openExternal = async (url: string) => { + return resolve(url); + }; + }); + }); + } + + public async destroy() { + await this._page.close(); + await this._app.close() + } +} diff --git a/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts b/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts new file mode 100644 index 00000000..6b587969 --- /dev/null +++ b/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts @@ -0,0 +1,33 @@ +import { _electron as electron, test as base } from '@playwright/test'; +import { AuthFixture } from './AuthFixture'; +import { ElectronAppFixture } from './ElectronAppFixture'; +import { RefreshTokenStore } from '../../main/TokenStore'; +import { getElectronUserDataPath, getTokenStoreFileName, getTokenStoreKey } from '../helpers/utils'; + +const tokenStoreFileName = getTokenStoreFileName(); +const tokenStoreKey = getTokenStoreKey(); +const userDataPath = getElectronUserDataPath(); + +export const test = base.extend<{ auth: AuthFixture, app: ElectronAppFixture }>({ + app: async ({ }, use) => { + const tokenStore = new RefreshTokenStore(tokenStoreFileName, tokenStoreKey, userDataPath); + await tokenStore.delete(); + const electronApp = await electron.launch({ + args: ["./dist/integration-test/test-app/index.js"], + }); + const electronPage = await electronApp.firstWindow(); + const app = new ElectronAppFixture(electronPage, electronApp); + await use(app); + await app.destroy(); + }, + auth: async ({ browser }, use) => { + const page = await browser.newPage(); + const auth = new AuthFixture({ + page, + tokenStore: new RefreshTokenStore(tokenStoreFileName, tokenStoreKey, userDataPath) + }); + await use(auth); + }, +}); + +export { expect } from '@playwright/test'; \ No newline at end of file diff --git a/packages/electron/src/integration-test/helpers/TestHelper.ts b/packages/electron/src/integration-test/helpers/TestHelper.ts deleted file mode 100644 index d7b2db66..00000000 --- a/packages/electron/src/integration-test/helpers/TestHelper.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ - -import type { Page } from "@playwright/test"; -import type { SignInOptions } from "../types"; - -/** - * Helper class for tests - */ -export class TestHelper { - constructor(private _signInOptions: SignInOptions) { } - - public async clickSignIn(electronPage: Page) { - await electronPage.waitForSelector("button#signIn"); - const button = electronPage.getByTestId("signIn"); - await button.click(); - } - - public async clickSignOut(electronPage: Page) { - await electronPage.waitForSelector("button#signOut"); - const button = electronPage.getByTestId("signOut"); - await button.click(); - } - - public async isSignedIn(electronPage: Page) { - const button = electronPage.getByTestId("getStatus"); - await button.click(); - const locator = electronPage.getByText("signed in"); - return locator.isVisible(); - } - public async checkStatus(electronPage: Page, expectedStatus: boolean) { - await electronPage.waitForSelector("button#getStatus"); - const button = electronPage.getByTestId("getStatus"); - await button.click(); - electronPage.getByText(expectedStatus ? "signed in" : "signed out"); - } - - public async signIn(page: Page, url: string) { - await page.goto(url); - await page.waitForSelector("#identifierInput", { timeout: 5000 }); - await page.getByLabel("Email address").fill(this._signInOptions.email); - await page.getByLabel("Email address").press("Enter"); - await page.getByLabel("Password").fill(this._signInOptions.password); - await page.getByText("Sign In").click(); - - const consentUrl = page.url(); - if (consentUrl.endsWith("resume/as/authorization.ping")) { - await this.handleConsentScreen(page); - } - } - - private async handleConsentScreen(page: Page) { - const consentAcceptButton = page.getByRole("link", { - name: "Accept", - }); - if (consentAcceptButton) - await consentAcceptButton.click(); - } -} diff --git a/packages/electron/src/integration-test/helpers/utils.ts b/packages/electron/src/integration-test/helpers/utils.ts new file mode 100644 index 00000000..367079e2 --- /dev/null +++ b/packages/electron/src/integration-test/helpers/utils.ts @@ -0,0 +1,28 @@ +import { loadConfig } from "./loadConfig"; + +const { clientId, envPrefix } = loadConfig() + +// Get the user data path that would be returned in app.getPath('userData') if ran in main electron process. +export const getElectronUserDataPath = (): string | undefined => { + switch (process.platform) { + case "darwin": // For MacOS + return `${process.env.HOME}/Library/Application Support/Electron`; + case "win32": // For Windows + return `${process.env.APPDATA!}/Electron`; + case "linux": // For Linux + return undefined; // Linux uses the same path for both main and renderer processes, no need to manually resolve path. + default: + return process.cwd(); + } +}; + +export const getTokenStoreFileName = (): string => `iTwinJs _${clientId}`; + +export const getTokenStoreKey = (issuerUrl?: string): string => { + const authority = new URL(issuerUrl ?? "https://ims.bentley.com"); + if (envPrefix && !issuerUrl) { + authority.hostname = envPrefix + authority.hostname; + } + issuerUrl = authority.href.replace(/\/$/, ""); + return `${getTokenStoreFileName()}:${issuerUrl}`; +} diff --git a/packages/electron/src/integration-test/integration.test.ts b/packages/electron/src/integration-test/integration.test.ts index 346cc3ac..e23347aa 100644 --- a/packages/electron/src/integration-test/integration.test.ts +++ b/packages/electron/src/integration-test/integration.test.ts @@ -2,121 +2,38 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ - -import type { ElectronApplication, Page } from "@playwright/test"; -import { _electron as electron, expect, test } from "@playwright/test"; -import type { SignInOptions } from "./types"; import { loadConfig } from "./helpers/loadConfig"; -import { TestHelper } from "./helpers/TestHelper"; -import { RefreshTokenStore } from "../main/TokenStore"; - -const { clientId, envPrefix, email, password } = loadConfig(); - -const signInOptions: SignInOptions = { - clientId, - email, - password, - envPrefix, -}; - -// Get the user data path that would be returned in app.getPath('userData') if ran in main electron process. -const getElectronUserDataPath = (): string | undefined => { - switch (process.platform) { - case "darwin": // For MacOS - return `${process.env.HOME}/Library/Application Support/Electron`; - case "win32": // For Windows - return `${process.env.APPDATA!}/Electron`; - case "linux": // For Linux - return undefined; // Linux uses the same path for both main and renderer processes, no need to manually resolve path. - default: - return process.cwd(); - } -}; - -const userDataPath = getElectronUserDataPath(); -let electronApp: ElectronApplication; -let electronPage: Page; -const testHelper = new TestHelper(signInOptions); -const tokenStore = new RefreshTokenStore(getTokenStoreFileName(), getTokenStoreKey(), userDataPath); +import { test, expect } from "./fixtures/ElectronFixtures"; +const { email, password } = loadConfig(); -function getTokenStoreKey(issuerUrl?: string): string { - const authority = new URL(issuerUrl ?? "https://ims.bentley.com"); - if (envPrefix && !issuerUrl) { - authority.hostname = envPrefix + authority.hostname; - } - issuerUrl = authority.href.replace(/\/$/, ""); - return `${getTokenStoreFileName()}:${issuerUrl}`; -} - -function getTokenStoreFileName(): string { - return `iTwinJs_${clientId}`; -} - -async function getUrl(app: ElectronApplication): Promise { - // evaluates in the context of the main process - // TODO: consider writing a helper to make this easier - return app.evaluate(async ({ shell }) => { - return new Promise((resolve) => { - shell.openExternal = async (url: string) => { - return resolve(url); - }; - }); - }); -} - -test.beforeEach(async () => { - try { - await tokenStore.delete(); - electronApp = await electron.launch({ - args: ["./dist/integration-test/test-app/index.js"], - }); - electronPage = await electronApp.firstWindow(); - } catch (error) { - } +test("buttons exist", async ({ app }) => { + await expect(app.buttons.signIn).toBeVisible(); + await expect(app.buttons.signOut).toBeVisible(); + await expect(app.buttons.status).toBeVisible(); }); -test.afterEach(async () => { - await electronApp.close(); -}); +test("sign in successful", async ({ app, auth }) => { + await app.checkStatus(false); -test("buttons exist", async () => { - const signInButton = electronPage.getByTestId("signIn"); - const signOutButton = electronPage.getByTestId("signOut"); - const getStatusButton = electronPage.getByTestId("getStatus"); - await expect(signInButton).toBeVisible(); - await expect(signOutButton).toBeVisible(); - await expect(getStatusButton).toBeVisible(); + const url = await app.clickSignIn(); + await auth.signInIMS(url, email, password); + await app.checkStatus(true); }); -test("sign in successful", async ({ browser }) => { - const page = await browser.newPage(); - await testHelper.checkStatus(electronPage, false); - await testHelper.clickSignIn(electronPage); - const url = await getUrl(electronApp); - await testHelper.signIn(page, url); - await testHelper.checkStatus(electronPage, true); - await page.close(); -}); +test("sign out successful", async ({ app, auth }) => { + const url = await app.clickSignIn(); + await auth.signInIMS(url, email, password); + await app.checkStatus(true); -test("sign out successful", async ({ browser }) => { - const page = await browser.newPage(); - await testHelper.clickSignIn(electronPage); - await testHelper.signIn(page, await getUrl(electronApp)); - await testHelper.checkStatus(electronPage, true); - await testHelper.clickSignOut(electronPage); - await testHelper.checkStatus(electronPage, false); - await page.close(); + await app.clickSignOut(); + await app.checkStatus(false) }); -test("when scopes change, sign in is required", async ({ browser }) => { - const page = await browser.newPage(); - await testHelper.clickSignIn(electronPage); - await testHelper.signIn(page, await getUrl(electronApp)); - await testHelper.checkStatus(electronPage, true); +test("when scopes change, sign in is required", async ({ app, auth }) => { + const url = await app.clickSignIn(); + await auth.signInIMS(url, email, password); + await app.checkStatus(true); - // Admittedly this is cheating: no user would interact - // with the tokenStore directly, but this is a tough - // case to test otherwise. - await tokenStore.load("itwin-platform realitydata:read"); - await testHelper.checkStatus(electronPage, false); + await auth.switchScopes("itwin-platform realitydata:read") + await app.checkStatus(false); }); diff --git a/packages/electron/src/integration-test/types.ts b/packages/electron/src/integration-test/types.ts deleted file mode 100644 index cb5a01da..00000000 --- a/packages/electron/src/integration-test/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ - -/** - * Options for signing in. - */ -export interface SignInOptions { - clientId: string; - email: string; - password: string; - envPrefix: string; -} From 0900c84fea7f3252ed1b91a4163ac637559af1bb Mon Sep 17 00:00:00 2001 From: Ben Polinsky Date: Tue, 17 Dec 2024 14:28:07 -0500 Subject: [PATCH 2/5] add browser intergration test fixtures; lil work on electron's --- .../BrowserAppFixture.ts} | 67 +++++++++-- .../fixtures/BrowserFixtures.ts | 23 ++++ .../integration-tests/helpers/loadConfig.ts | 1 - .../src/integration-tests/integration.test.ts | 105 +++++------------- packages/electron/eslint.config.js | 3 +- .../integration-test/fixtures/AuthFixture.ts | 6 +- .../fixtures/ElectronAppFixture.ts | 19 +--- .../fixtures/ElectronFixtures.ts | 14 +-- .../src/integration-test/helpers/utils.ts | 4 +- .../src/integration-test/integration.test.ts | 6 +- 10 files changed, 129 insertions(+), 119 deletions(-) rename packages/browser/src/integration-tests/{helpers/TestHelper.ts => fixtures/BrowserAppFixture.ts} (50%) create mode 100644 packages/browser/src/integration-tests/fixtures/BrowserFixtures.ts diff --git a/packages/browser/src/integration-tests/helpers/TestHelper.ts b/packages/browser/src/integration-tests/fixtures/BrowserAppFixture.ts similarity index 50% rename from packages/browser/src/integration-tests/helpers/TestHelper.ts rename to packages/browser/src/integration-tests/fixtures/BrowserAppFixture.ts index 212999b8..ae940e9d 100644 --- a/packages/browser/src/integration-tests/helpers/TestHelper.ts +++ b/packages/browser/src/integration-tests/fixtures/BrowserAppFixture.ts @@ -9,10 +9,18 @@ import { User } from "oidc-client-ts"; import { AuthType } from "../types"; import type { SignInOptions } from "../types"; -export class TestHelper { - constructor(private _signInOptions: SignInOptions) {} +export class BrowserAppFixture { + constructor(private _page: Page, private _signInOptions: SignInOptions) { } - public async signIn(page: Page) { + get routes() { + return { + staticCallback: `${this._signInOptions.url}?callbackFromStorage=true`, + authViaPopup: `${this._signInOptions.url}/signin-via-popup`, + root: `${this._signInOptions.url}`, + } + } + + public async signIn(page: Page = this._page) { await page.getByLabel("Email address").fill(this._signInOptions.email); await page.getByLabel("Email address").press("Enter"); await page.getByLabel("Password").fill(this._signInOptions.password); @@ -24,8 +32,35 @@ export class TestHelper { } } - public async getUserFromLocalStorage(page: Page): Promise { - const storageState = await page.context().storageState(); + public async signInViaPopup() { + await this._page.goto(this.routes.authViaPopup); + const popupPromise = this._page.waitForEvent("popup"); + const el = this._page.getByText("Signin via Popup"); + await el.click(); + const popup = await popupPromise; + await popup.waitForLoadState(); + + const signInPromise = this.signIn(popup); + const closeEventPromise = popup.waitForEvent("close"); + + await Promise.all([signInPromise, closeEventPromise]); + } + + public async signOutViaPopup() { + const signoutPopupPromise = this._page.waitForEvent("popup"); + const locator = this._page.getByTestId("signout-button-popup"); + await locator.click(); + const signOutPopup = await signoutPopupPromise; + return signOutPopup + } + + public async signOut() { + const locator = this._page.getByTestId("signout-button"); + await locator.click(); + } + + public async getUserFromLocalStorage(): Promise { + const storageState = await this._page.context().storageState(); const localStorage = storageState.origins.find( (o) => o.origin === this._signInOptions.url )?.localStorage; @@ -44,12 +79,11 @@ export class TestHelper { } public async validateAuthenticated( - page: Page, authType: AuthType = AuthType.Redirect ) { - const locator = page.getByTestId("content"); + const locator = this._page.getByTestId("content"); await expect(locator).toContainText("Authorized"); - const user = await this.getUserFromLocalStorage(page); + const user = await this.getUserFromLocalStorage(); expect(user.access_token).toBeDefined(); let url = `${this._signInOptions.url}/`; @@ -58,10 +92,23 @@ export class TestHelper { if (authType === AuthType.RedirectStatic) url = "http://localhost:5173/?callbackFromStorage=true"; - expect(page.url()).toEqual(url); + expect(this._page.url()).toEqual(url); + } + + public async validateUnauthenticated(page: Page = this._page) { + const content = page.getByText("Sign Off Successful"); + await expect(content).toBeVisible(); + } + + public async goToPage(url: string) { + await this._page.goto(url); + } + + public async waitForPageLoad(url: string = this._signInOptions.url, page: Page = this._page) { + await page.waitForURL(url); } - private async handleConsentScreen(page: Page) { + private async handleConsentScreen(page: Page = this._page) { const consentAcceptButton = page.getByRole("link", { name: "Accept", }); diff --git a/packages/browser/src/integration-tests/fixtures/BrowserFixtures.ts b/packages/browser/src/integration-tests/fixtures/BrowserFixtures.ts new file mode 100644 index 00000000..55896127 --- /dev/null +++ b/packages/browser/src/integration-tests/fixtures/BrowserFixtures.ts @@ -0,0 +1,23 @@ +import { test as base } from "@playwright/test"; +import { BrowserAppFixture } from "./BrowserAppFixture"; +import { loadConfig } from "../helpers/loadConfig"; + +const { url, clientId, envPrefix, email, password } = loadConfig(); + +const signInOptions = { + email, + password, + url, + clientId, + envPrefix: envPrefix || "", +}; + +export const test = base.extend<{ app: BrowserAppFixture }>({ + app: async ({ page }, use) => { + const testHelper = new BrowserAppFixture(page, signInOptions); + await use(testHelper); + }, +}); + +export { expect } from "@playwright/test"; + diff --git a/packages/browser/src/integration-tests/helpers/loadConfig.ts b/packages/browser/src/integration-tests/helpers/loadConfig.ts index 72f98454..54e7c3d1 100644 --- a/packages/browser/src/integration-tests/helpers/loadConfig.ts +++ b/packages/browser/src/integration-tests/helpers/loadConfig.ts @@ -3,7 +3,6 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -// eslint@typescript-eslint/naming-convention import { config } from "dotenv"; export function loadConfig() { diff --git a/packages/browser/src/integration-tests/integration.test.ts b/packages/browser/src/integration-tests/integration.test.ts index 72c99697..89b403c7 100644 --- a/packages/browser/src/integration-tests/integration.test.ts +++ b/packages/browser/src/integration-tests/integration.test.ts @@ -3,93 +3,40 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { expect, test } from "@playwright/test"; -import { TestHelper } from "./helpers/TestHelper"; import { AuthType } from "./types"; -import type { SignInOptions } from "./types"; -import { loadConfig } from "./helpers/loadConfig"; +import { test } from "./fixtures/BrowserFixtures"; -const { url, clientId, envPrefix, email, password } = loadConfig(); - -const signInOptions: SignInOptions = { - email, - password, - url, - clientId, - envPrefix: envPrefix || "", -}; - -const testHelper = new TestHelper(signInOptions); - -test("signin redirect", async ({ page }) => { - await page.goto(signInOptions.url); - await testHelper.signIn(page); - await page.waitForURL(signInOptions.url); - - await testHelper.validateAuthenticated(page); +test("signin redirect", async ({ app }) => { + await app.goToPage(app.routes.root) + await app.signIn(); + await app.waitForPageLoad(app.routes.root) + await app.validateAuthenticated(); }); -test("signin redirect - callback settings from storage", async ({ page }) => { - const staticCallbackUrl = `${signInOptions.url}?callbackFromStorage=true`; - await page.goto(staticCallbackUrl); - await testHelper.signIn(page); - await page.waitForURL(staticCallbackUrl); - - await testHelper.validateAuthenticated(page, AuthType.RedirectStatic); +test("signin redirect - callback settings from storage", async ({ app }) => { + await app.goToPage(app.routes.staticCallback); + await app.signIn(); + await app.waitForPageLoad(app.routes.staticCallback) + await app.validateAuthenticated(AuthType.RedirectStatic); }); -test("signout redirect", async ({ page }) => { - await page.goto(signInOptions.url); - await testHelper.signIn(page); - await page.waitForURL(signInOptions.url); - - const locator = page.getByTestId("signout-button"); - await locator.click(); - - const content = page.getByText("Sign Off Successful"); - expect(content).toBeDefined(); - - // Cannot get the below working on QA... - // We'll have to settle for the above check - // await expect(content).toContainText("Signed Out!"); - // const user = await testHelper.getUserFromLocalStorage(page); - // expect(user).not.toBeDefined(); +test("signout redirect", async ({ app }) => { + await app.goToPage(app.routes.root) + await app.signIn(); + await app.waitForPageLoad(app.routes.root) + await app.validateAuthenticated() + await app.signOut(); + await app.validateUnauthenticated() }); -test("signin popup", async ({ page }) => { - await page.goto(`${signInOptions.url}/signin-via-popup`); - const popupPromise = page.waitForEvent("popup"); - const el = page.getByText("Signin via Popup"); - await el.click(); - const popup = await popupPromise; - await popup.waitForLoadState(); - - const signInPromise = testHelper.signIn(popup); - const closeEventPromise = popup.waitForEvent("close"); - - await Promise.all([signInPromise, closeEventPromise]); - await testHelper.validateAuthenticated(page, AuthType.PopUp); +test("signin popup", async ({ app }) => { + await app.signInViaPopup(); + await app.validateAuthenticated(AuthType.PopUp); }); -test("signout popup", async ({ page }) => { - await page.goto(`${signInOptions.url}/signin-via-popup`); - const popupPromise = page.waitForEvent("popup"); - const el = page.getByText("Signin via Popup"); - await el.click(); - const popup = await popupPromise; - await popup.waitForLoadState(); - - const signInPromise = testHelper.signIn(popup); - const closeEventPromise = popup.waitForEvent("close"); - - await Promise.all([signInPromise, closeEventPromise]); - await testHelper.validateAuthenticated(page, AuthType.PopUp); - - const signoutPopupPromise = page.waitForEvent("popup"); - const locator = page.getByTestId("signout-button-popup"); - await locator.click(); - const signOutPopup = await signoutPopupPromise; - - const content = signOutPopup.getByText("Sign Off Successful"); - expect(content).toBeDefined(); +test("signout popup", async ({ app }) => { + await app.signInViaPopup(); + await app.validateAuthenticated(AuthType.PopUp); + const signOutPopup = await app.signOutViaPopup() + await app.validateUnauthenticated(signOutPopup); }); diff --git a/packages/electron/eslint.config.js b/packages/electron/eslint.config.js index 97f0aac8..3f453c1e 100644 --- a/packages/electron/eslint.config.js +++ b/packages/electron/eslint.config.js @@ -6,8 +6,9 @@ module.exports = [ ...iTwinPlugin.configs.iTwinjsRecommendedConfig, }, { - files: ["**/*.{ts,tsx}"], + files: ["src/**/*.{ts,tsx}"], ...iTwinPlugin.configs.jsdocConfig, + ignores: ["src/integration-test/**/*", "src/test/**/*"], }, { files: ["**/*.{ts,tsx}"], diff --git a/packages/electron/src/integration-test/fixtures/AuthFixture.ts b/packages/electron/src/integration-test/fixtures/AuthFixture.ts index 31e48ce1..5920c9ef 100644 --- a/packages/electron/src/integration-test/fixtures/AuthFixture.ts +++ b/packages/electron/src/integration-test/fixtures/AuthFixture.ts @@ -1,14 +1,14 @@ import type { Page } from "@playwright/test"; -import { RefreshTokenStore } from "../../main/TokenStore"; +import type { RefreshTokenStore } from "../../main/TokenStore"; interface AuthFixtureProps { page: Page; - tokenStore: RefreshTokenStore + tokenStore: RefreshTokenStore; } export class AuthFixture { private _page: Page; - private _tokenStore: RefreshTokenStore + private _tokenStore: RefreshTokenStore; constructor(options: AuthFixtureProps) { this._page = options.page; diff --git a/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts b/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts index 2832ccf5..5cf8932a 100644 --- a/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts +++ b/packages/electron/src/integration-test/fixtures/ElectronAppFixture.ts @@ -1,14 +1,14 @@ -import { ElectronApplication, Page } from "@playwright/test"; +import type { ElectronApplication, Page } from "@playwright/test"; export class ElectronAppFixture { constructor(private _page: Page, private _app: ElectronApplication) { } - get buttons() { + public get buttons() { return { signIn: this._page.getByTestId("signIn"), signOut: this._page.getByTestId("signOut"), - status: this._page.getByTestId("getStatus") - } + status: this._page.getByTestId("getStatus"), + }; } /** @@ -20,7 +20,7 @@ export class ElectronAppFixture { const clickPromise = button.click(); const urlPromise = this.getUrl(); const [, url] = await Promise.all([clickPromise, urlPromise]); - return url + return url; } public async clickSignOut() { @@ -29,13 +29,6 @@ export class ElectronAppFixture { await button.click(); } - public async isSignedIn() { - const button = this._page.getByTestId("getStatus"); - await button.click(); - const locator = this._page.getByText("signed in"); - return locator.isVisible(); - } - public async checkStatus(expectedStatus: boolean) { await this._page.waitForSelector("button#getStatus"); const button = this._page.getByTestId("getStatus"); @@ -58,6 +51,6 @@ export class ElectronAppFixture { public async destroy() { await this._page.close(); - await this._app.close() + await this._app.close(); } } diff --git a/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts b/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts index 6b587969..ff246b87 100644 --- a/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts +++ b/packages/electron/src/integration-test/fixtures/ElectronFixtures.ts @@ -1,8 +1,8 @@ -import { _electron as electron, test as base } from '@playwright/test'; -import { AuthFixture } from './AuthFixture'; -import { ElectronAppFixture } from './ElectronAppFixture'; -import { RefreshTokenStore } from '../../main/TokenStore'; -import { getElectronUserDataPath, getTokenStoreFileName, getTokenStoreKey } from '../helpers/utils'; +import { test as base, _electron as electron } from "@playwright/test"; +import { AuthFixture } from "./AuthFixture"; +import { ElectronAppFixture } from "./ElectronAppFixture"; +import { RefreshTokenStore } from "../../main/TokenStore"; +import { getElectronUserDataPath, getTokenStoreFileName, getTokenStoreKey } from "../helpers/utils"; const tokenStoreFileName = getTokenStoreFileName(); const tokenStoreKey = getTokenStoreKey(); @@ -24,10 +24,10 @@ export const test = base.extend<{ auth: AuthFixture, app: ElectronAppFixture }>( const page = await browser.newPage(); const auth = new AuthFixture({ page, - tokenStore: new RefreshTokenStore(tokenStoreFileName, tokenStoreKey, userDataPath) + tokenStore: new RefreshTokenStore(tokenStoreFileName, tokenStoreKey, userDataPath), }); await use(auth); }, }); -export { expect } from '@playwright/test'; \ No newline at end of file +export { expect } from "@playwright/test"; diff --git a/packages/electron/src/integration-test/helpers/utils.ts b/packages/electron/src/integration-test/helpers/utils.ts index 367079e2..0df30def 100644 --- a/packages/electron/src/integration-test/helpers/utils.ts +++ b/packages/electron/src/integration-test/helpers/utils.ts @@ -1,6 +1,6 @@ import { loadConfig } from "./loadConfig"; -const { clientId, envPrefix } = loadConfig() +const { clientId, envPrefix } = loadConfig(); // Get the user data path that would be returned in app.getPath('userData') if ran in main electron process. export const getElectronUserDataPath = (): string | undefined => { @@ -25,4 +25,4 @@ export const getTokenStoreKey = (issuerUrl?: string): string => { } issuerUrl = authority.href.replace(/\/$/, ""); return `${getTokenStoreFileName()}:${issuerUrl}`; -} +}; diff --git a/packages/electron/src/integration-test/integration.test.ts b/packages/electron/src/integration-test/integration.test.ts index e23347aa..057810cd 100644 --- a/packages/electron/src/integration-test/integration.test.ts +++ b/packages/electron/src/integration-test/integration.test.ts @@ -3,7 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { loadConfig } from "./helpers/loadConfig"; -import { test, expect } from "./fixtures/ElectronFixtures"; +import { expect, test } from "./fixtures/ElectronFixtures"; const { email, password } = loadConfig(); test("buttons exist", async ({ app }) => { @@ -26,7 +26,7 @@ test("sign out successful", async ({ app, auth }) => { await app.checkStatus(true); await app.clickSignOut(); - await app.checkStatus(false) + await app.checkStatus(false); }); test("when scopes change, sign in is required", async ({ app, auth }) => { @@ -34,6 +34,6 @@ test("when scopes change, sign in is required", async ({ app, auth }) => { await auth.signInIMS(url, email, password); await app.checkStatus(true); - await auth.switchScopes("itwin-platform realitydata:read") + await auth.switchScopes("itwin-platform realitydata:read"); await app.checkStatus(false); }); From afc9415ada5b6c0790ce5f934d341fad095d2dd7 Mon Sep 17 00:00:00 2001 From: Ben Polinsky Date: Tue, 17 Dec 2024 14:37:27 -0500 Subject: [PATCH 3/5] Change files --- ...authorization-bc141240-897b-4138-a46e-ac030df3b800.json | 7 +++++++ ...authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json create mode 100644 change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json diff --git a/change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json b/change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json new file mode 100644 index 00000000..fe28ac76 --- /dev/null +++ b/change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "browser integration test fixtures", + "packageName": "@itwin/browser-authorization", + "email": "ben-polinsky@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json b/change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json new file mode 100644 index 00000000..8e6f8360 --- /dev/null +++ b/change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "electron integration test fixtures", + "packageName": "@itwin/electron-authorization", + "email": "ben-polinsky@users.noreply.github.com", + "dependentChangeType": "none" +} From 095f1dd489929b865d00f6c9a2ac598aff716209 Mon Sep 17 00:00:00 2001 From: Ben Polinsky Date: Tue, 17 Dec 2024 14:50:10 -0500 Subject: [PATCH 4/5] update change config --- beachball.config.js | 3 ++- ...authorization-bc141240-897b-4138-a46e-ac030df3b800.json | 7 ------- ...authorization-cc3b2112-dbce-491a-96ac-6796f9258e4c.json | 7 ------- ...authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json | 7 ------- ...authorization-d50363a6-f07e-422d-86b9-3f841e25f227.json | 7 ------- 5 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json delete mode 100644 change/@itwin-browser-authorization-cc3b2112-dbce-491a-96ac-6796f9258e4c.json delete mode 100644 change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json delete mode 100644 change/@itwin-electron-authorization-d50363a6-f07e-422d-86b9-3f841e25f227.json diff --git a/beachball.config.js b/beachball.config.js index f140aa40..f5423d09 100644 --- a/beachball.config.js +++ b/beachball.config.js @@ -9,12 +9,13 @@ module.exports = { tag: "latest", ignorePatterns: [ ".nycrc", - ".eslintrc.json", + "eslint.config.js", "tsconfig.*", ".*ignore", ".github/**", ".vscode/**", "pnpm-lock.yaml", + "**/integration-test*/**", ], changehint: "Run 'pnpm change' to generate a change file", }; \ No newline at end of file diff --git a/change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json b/change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json deleted file mode 100644 index fe28ac76..00000000 --- a/change/@itwin-browser-authorization-bc141240-897b-4138-a46e-ac030df3b800.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "browser integration test fixtures", - "packageName": "@itwin/browser-authorization", - "email": "ben-polinsky@users.noreply.github.com", - "dependentChangeType": "none" -} diff --git a/change/@itwin-browser-authorization-cc3b2112-dbce-491a-96ac-6796f9258e4c.json b/change/@itwin-browser-authorization-cc3b2112-dbce-491a-96ac-6796f9258e4c.json deleted file mode 100644 index 19f9d4a5..00000000 --- a/change/@itwin-browser-authorization-cc3b2112-dbce-491a-96ac-6796f9258e4c.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "remove parcel from browser e2e tests", - "packageName": "@itwin/browser-authorization", - "email": "ben-polinsky@users.noreply.github.com", - "dependentChangeType": "none" -} diff --git a/change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json b/change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json deleted file mode 100644 index 8e6f8360..00000000 --- a/change/@itwin-electron-authorization-9e7c4fc5-ef34-4ae7-b2e3-4311323d31b6.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "electron integration test fixtures", - "packageName": "@itwin/electron-authorization", - "email": "ben-polinsky@users.noreply.github.com", - "dependentChangeType": "none" -} diff --git a/change/@itwin-electron-authorization-d50363a6-f07e-422d-86b9-3f841e25f227.json b/change/@itwin-electron-authorization-d50363a6-f07e-422d-86b9-3f841e25f227.json deleted file mode 100644 index c34a4961..00000000 --- a/change/@itwin-electron-authorization-d50363a6-f07e-422d-86b9-3f841e25f227.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "remove networkidle from electron integration tests", - "packageName": "@itwin/electron-authorization", - "email": "ben-polinsky@users.noreply.github.com", - "dependentChangeType": "none" -} From 2f0f0cfb58a77a256e842c983d2f7ac64167935f Mon Sep 17 00:00:00 2001 From: Ben Polinsky Date: Tue, 17 Dec 2024 14:51:29 -0500 Subject: [PATCH 5/5] exclude unit tests from triggering changes as well --- beachball.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/beachball.config.js b/beachball.config.js index f5423d09..ccd98286 100644 --- a/beachball.config.js +++ b/beachball.config.js @@ -16,6 +16,7 @@ module.exports = { ".vscode/**", "pnpm-lock.yaml", "**/integration-test*/**", + "**/test/**", ], changehint: "Run 'pnpm change' to generate a change file", }; \ No newline at end of file