Skip to content

Commit

Permalink
add browser intergration test fixtures; lil work on electron's
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-polinsky committed Dec 17, 2024
1 parent d81d4ad commit 0900c84
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -24,8 +32,35 @@ export class TestHelper {
}
}

public async getUserFromLocalStorage(page: Page): Promise<User> {
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<User> {
const storageState = await this._page.context().storageState();
const localStorage = storageState.origins.find(
(o) => o.origin === this._signInOptions.url
)?.localStorage;
Expand All @@ -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}/`;
Expand All @@ -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",
});
Expand Down
23 changes: 23 additions & 0 deletions packages/browser/src/integration-tests/fixtures/BrowserFixtures.ts
Original file line number Diff line number Diff line change
@@ -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";

Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
105 changes: 26 additions & 79 deletions packages/browser/src/integration-tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
3 changes: 2 additions & 1 deletion packages/electron/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}"],
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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"),
};
}

/**
Expand All @@ -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() {
Expand All @@ -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");
Expand All @@ -58,6 +51,6 @@ export class ElectronAppFixture {

public async destroy() {
await this._page.close();
await this._app.close()
await this._app.close();
}
}
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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';
export { expect } from "@playwright/test";
4 changes: 2 additions & 2 deletions packages/electron/src/integration-test/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -25,4 +25,4 @@ export const getTokenStoreKey = (issuerUrl?: string): string => {
}
issuerUrl = authority.href.replace(/\/$/, "");
return `${getTokenStoreFileName()}:${issuerUrl}`;
}
};
6 changes: 3 additions & 3 deletions packages/electron/src/integration-test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -26,14 +26,14 @@ 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 }) => {
const url = await app.clickSignIn();
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);
});

0 comments on commit 0900c84

Please sign in to comment.