diff --git a/libs/playwright-helpers/src/index.ts b/libs/playwright-helpers/src/index.ts index eb7f490ca84..bd6c06050fd 100644 --- a/libs/playwright-helpers/src/index.ts +++ b/libs/playwright-helpers/src/index.ts @@ -1,3 +1,5 @@ +export * from "./play"; export * from "./scene"; export * from "./scene-templates"; export { test } from "./test"; +export * from "./queries"; diff --git a/libs/playwright-helpers/src/play.ts b/libs/playwright-helpers/src/play.ts new file mode 100644 index 00000000000..675cacdc90e --- /dev/null +++ b/libs/playwright-helpers/src/play.ts @@ -0,0 +1,57 @@ +import { Query } from "./queries/query"; +import { + SceneOptions, + Scene, + SCENE_OPTIONS_DEFAULTS, + seedIdsToTearDown, + seedIdsToWarnAbout, +} from "./scene"; +import { SceneTemplate } from "./scene-templates/scene-template"; + +export class Play { + /** + * Runs server-side code to create a test scene and automatically destroys the scene when disposed. + * + * Scenes also expose a `mangle` method that can be used to mangle magic string in the same way the server reports them + * back to avoid collisions. For example, if a scene creates a user with the email `test@example.com`, you can call + * `scene.mangle("test@example.com")` to get the actual email address of the user created in the scene. + * + * Example usage: + * ```ts + * import { Play, SingleUserScene } from "@bitwarden/playwright-helpers"; + * + * test("my test", async ({ page }) => { + * using scene = await Play.scene(new SingleUserScene({ email: " + * expect(scene.mangle("my-id")).not.toBe("my-id"); + * }); + * + * @param template The template to run to create the scene + * @param options Options for the scene + * @returns + */ + static async scene( + template: SceneTemplate, + options: SceneOptions = {}, + ): Promise> { + const opts = { ...SCENE_OPTIONS_DEFAULTS, ...options }; + if (opts.noDown && process.env.CI) { + throw new Error("Cannot set noDown to true in CI environments"); + } + const scene = new Scene(opts); + await scene.init(template); + if (!opts.noDown) { + seedIdsToTearDown.add(scene.seedId); + } else { + seedIdsToWarnAbout.add(scene.seedId); + } + return scene; + } + + static async DeleteAllScenes(): Promise { + await Scene.DeleteAllScenes(); + } + + static async query(template: Query): Promise { + return await template.fetch(); + } +} diff --git a/libs/playwright-helpers/src/queries/emergency-access-invite.query.ts b/libs/playwright-helpers/src/queries/emergency-access-invite.query.ts new file mode 100644 index 00000000000..a9f6e7b4354 --- /dev/null +++ b/libs/playwright-helpers/src/queries/emergency-access-invite.query.ts @@ -0,0 +1,10 @@ +import { Query } from "./query"; + +export class EmergencyAccessInviteQuery extends Query< + { + email: string; + }, + string[] +> { + template: string = "EmergencyAccessInviteQuery"; +} diff --git a/libs/playwright-helpers/src/queries/index.ts b/libs/playwright-helpers/src/queries/index.ts new file mode 100644 index 00000000000..83e991e676e --- /dev/null +++ b/libs/playwright-helpers/src/queries/index.ts @@ -0,0 +1,5 @@ +/** + * Scene Templates represent server-side seed scenes to create data for tests. + */ + +export * from "./emergency-access-invite.query"; diff --git a/libs/playwright-helpers/src/queries/query.ts b/libs/playwright-helpers/src/queries/query.ts new file mode 100644 index 00000000000..4357c553d7a --- /dev/null +++ b/libs/playwright-helpers/src/queries/query.ts @@ -0,0 +1,33 @@ +import { webServerBaseUrl } from "@playwright-config"; + +// First seed points at the seeder API proxy, second is the query path of the QueryController +const queryApiUrl = new URL("/seed/query", webServerBaseUrl).toString(); + +export abstract class Query { + abstract template: string; + + constructor(private upArgs: TUp) {} + async fetch(): Promise { + const result = await queryFetch(this.template, this.upArgs); + return result; + } +} + +async function queryFetch(template: string, args: TUp): Promise { + const response = await fetch(queryApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + template: template, + arguments: args, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to run query: ${response.statusText}`); + } + + return (await response.json()) as TReturns; +} diff --git a/libs/playwright-helpers/src/scene-templates/emergency-access-invite.scene.ts b/libs/playwright-helpers/src/scene-templates/emergency-access-invite.scene.ts deleted file mode 100644 index 63e17ada054..00000000000 --- a/libs/playwright-helpers/src/scene-templates/emergency-access-invite.scene.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SceneTemplate } from "./scene-template"; - -export class EmergencyAccessInviteQuery extends SceneTemplate<{ - email: string; -}> { - template: string = "EmergencyAccessInviteQuery"; -} diff --git a/libs/playwright-helpers/src/scene-templates/index.ts b/libs/playwright-helpers/src/scene-templates/index.ts index 1b09b273f86..4090a6f6ff1 100644 --- a/libs/playwright-helpers/src/scene-templates/index.ts +++ b/libs/playwright-helpers/src/scene-templates/index.ts @@ -2,5 +2,4 @@ * Scene Templates represent server-side seed scenes to create data for tests. */ -export * from "./emergency-access-invite.scene"; export * from "./single-user.scene"; diff --git a/libs/playwright-helpers/src/scene.ts b/libs/playwright-helpers/src/scene.ts index f8405996fa8..a6e6a08a01d 100644 --- a/libs/playwright-helpers/src/scene.ts +++ b/libs/playwright-helpers/src/scene.ts @@ -113,6 +113,16 @@ export class Scene implements UsingRequired { this.mangledMap = new Map(Object.entries(result.mangleMap)); this._returnValue = result.result as unknown as Returns; } + + static async DeleteAllScenes(): Promise { + const response = await fetch(seedApiUrl, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error(`Failed to delete scenes: ${response.statusText}`); + } + } } export type SceneOptions = { @@ -134,63 +144,13 @@ export type SceneOptions = { downAfterAll?: boolean; }; -const SCENE_OPTIONS_DEFAULTS: Readonly = Object.freeze({ +export const SCENE_OPTIONS_DEFAULTS: Readonly = Object.freeze({ noDown: false, downAfterAll: false, }); -export class Play { - /** - * Runs server-side code to create a test scene and automatically destroys the scene when disposed. - * - * Scenes also expose a `mangle` method that can be used to mangle magic string in the same way the server reports them - * back to avoid collisions. For example, if a scene creates a user with the email `test@example.com`, you can call - * `scene.mangle("test@example.com")` to get the actual email address of the user created in the scene. - * - * Example usage: - * ```ts - * import { Play, SingleUserScene } from "@bitwarden/playwright-helpers"; - * - * test("my test", async ({ page }) => { - * using scene = await Play.scene(new SingleUserScene({ email: " - * expect(scene.mangle("my-id")).not.toBe("my-id"); - * }); - * - * @param template The template to run to create the scene - * @param options Options for the scene - * @returns - */ - static async scene( - template: SceneTemplate, - options: SceneOptions = {}, - ): Promise> { - const opts = { ...SCENE_OPTIONS_DEFAULTS, ...options }; - if (opts.noDown && process.env.CI) { - throw new Error("Cannot set noDown to true in CI environments"); - } - const scene = new Scene(opts); - await scene.init(template); - if (!opts.noDown) { - seedIdsToTearDown.add(scene.seedId); - } else { - seedIdsToWarnAbout.add(scene.seedId); - } - return scene; - } - - static async DeleteAllScenes(): Promise { - const response = await fetch(seedApiUrl, { - method: "DELETE", - }); - - if (!response.ok) { - throw new Error(`Failed to delete scenes: ${response.statusText}`); - } - } -} - -const seedIdsToTearDown = new Set(); -const seedIdsToWarnAbout = new Set(); +export const seedIdsToTearDown = new Set(); +export const seedIdsToWarnAbout = new Set(); // After all tests complete test.afterAll(async () => {