1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

Create queries for grabbing data from the server

This commit is contained in:
Matt Gibson
2025-10-29 16:52:57 -07:00
parent bc9e40dbd0
commit 37201585df
8 changed files with 120 additions and 61 deletions

View File

@@ -1,3 +1,5 @@
export * from "./play";
export * from "./scene";
export * from "./scene-templates";
export { test } from "./test";
export * from "./queries";

View File

@@ -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<TUp, TResult>(
template: SceneTemplate<TUp, TResult>,
options: SceneOptions = {},
): Promise<Scene<TResult>> {
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<TResult>(opts);
await scene.init(template);
if (!opts.noDown) {
seedIdsToTearDown.add(scene.seedId);
} else {
seedIdsToWarnAbout.add(scene.seedId);
}
return scene;
}
static async DeleteAllScenes(): Promise<void> {
await Scene.DeleteAllScenes();
}
static async query<TUp, TReturns>(template: Query<TUp, TReturns>): Promise<TReturns> {
return await template.fetch();
}
}

View File

@@ -0,0 +1,10 @@
import { Query } from "./query";
export class EmergencyAccessInviteQuery extends Query<
{
email: string;
},
string[]
> {
template: string = "EmergencyAccessInviteQuery";
}

View File

@@ -0,0 +1,5 @@
/**
* Scene Templates represent server-side seed scenes to create data for tests.
*/
export * from "./emergency-access-invite.query";

View File

@@ -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<TUp, TReturns> {
abstract template: string;
constructor(private upArgs: TUp) {}
async fetch(): Promise<TReturns> {
const result = await queryFetch<TUp, TReturns>(this.template, this.upArgs);
return result;
}
}
async function queryFetch<TUp, TReturns>(template: string, args: TUp): Promise<TReturns> {
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;
}

View File

@@ -1,7 +0,0 @@
import { SceneTemplate } from "./scene-template";
export class EmergencyAccessInviteQuery extends SceneTemplate<{
email: string;
}> {
template: string = "EmergencyAccessInviteQuery";
}

View File

@@ -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";

View File

@@ -113,6 +113,16 @@ export class Scene<Returns = void> implements UsingRequired {
this.mangledMap = new Map(Object.entries(result.mangleMap));
this._returnValue = result.result as unknown as Returns;
}
static async DeleteAllScenes(): Promise<void> {
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<SceneOptions> = Object.freeze({
export const SCENE_OPTIONS_DEFAULTS: Readonly<SceneOptions> = 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<TUp, TResult>(
template: SceneTemplate<TUp, TResult>,
options: SceneOptions = {},
): Promise<Scene<TResult>> {
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<TResult>(opts);
await scene.init(template);
if (!opts.noDown) {
seedIdsToTearDown.add(scene.seedId);
} else {
seedIdsToWarnAbout.add(scene.seedId);
}
return scene;
}
static async DeleteAllScenes(): Promise<void> {
const response = await fetch(seedApiUrl, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(`Failed to delete scenes: ${response.statusText}`);
}
}
}
const seedIdsToTearDown = new Set<string>();
const seedIdsToWarnAbout = new Set<string>();
export const seedIdsToTearDown = new Set<string>();
export const seedIdsToWarnAbout = new Set<string>();
// After all tests complete
test.afterAll(async () => {