Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:

- name: Build dashboard
run: yarn build:dashboard
env:
VITE_SERVER_BASE_URL: http://localhost:8000

- name: Start Backend Server
run: docker compose -f ./docker-compose-test.yml up -d
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ dist-ssr
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"zustand": "^4.4.5"
},
"devDependencies": {
"@faker-js/faker": "^9.0.1",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^3.0.4",
Expand Down
19 changes: 13 additions & 6 deletions apps/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import { ONBOARDED_USER_FILE } from "./tests/utils/constants";

/**
* Read environment variables from file.
Expand All @@ -12,14 +13,15 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
testDir: "./tests",
timeout: 90000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,

retries: 3,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
workers: process.env.CI ? undefined : "80%",
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
Expand All @@ -33,19 +35,24 @@ export default defineConfig({

/* Configure projects for major browsers */
projects: [
{ name: "setup", testMatch: /.*\.setup\.ts/ },

{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
use: { ...devices["Desktop Chrome"], storageState: ONBOARDED_USER_FILE },
dependencies: ["setup"],
},

{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
use: { ...devices["Desktop Firefox"], storageState: ONBOARDED_USER_FILE },
dependencies: ["setup"],
},

{
name: "webkit",
use: { ...devices["Desktop Safari"] },
use: { ...devices["Desktop Safari"], storageState: ONBOARDED_USER_FILE },
dependencies: ["setup"],
},

/* Test against mobile viewports. */
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/layout/partials/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const Navbar = () => {
</div>
<div className="space-y-[27px] pb-[24px]">
<button
data-testid="create-survey"
onClick={() =>
navigate(
`${ROUTES.SURVEY_LIST}/?create=2`
Expand Down
11 changes: 9 additions & 2 deletions apps/frontend/src/layout/partials/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export const UserProfile = () => {

return (
<DropdownMenu>
<DropdownMenuTrigger className="flex w-full items-center text-intg-text outline-none">
<DropdownMenuTrigger
data-testid="profile-btn"
className="flex w-full items-center text-intg-text outline-none"
>
<div className="flex items-center gap-2">
<img src={Frame} alt="picture frame" className="h-[31px] w-[31px] rounded object-contain" />
<span className="text-sm">Profile</span>
Expand Down Expand Up @@ -174,7 +177,11 @@ export const UserProfile = () => {
);
})}
<DropdownMenuSeparator className="my-3 border-[.5px] border-intg-bg-4" />
<DropdownMenuItem className="flex cursor-pointer items-center gap-2 px-3 py-2" onClick={logout}>
<DropdownMenuItem
data-testid="logout"
className="flex cursor-pointer items-center gap-2 px-3 py-2"
onClick={logout}
>
<LogoutIcon />
<p className="text-sm text-intg-error-text">Log out</p>
</DropdownMenuItem>
Expand Down
99 changes: 99 additions & 0 deletions apps/frontend/tests/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { faker } from "@faker-js/faker";
import { expect, Page, test as setup } from "@playwright/test";
import {
NEW_USER_FILE,
NON_ONBOARDED_USER_FILE,
ONBOARDED_USER_FILE,
ROUTES,
userCredentials,
} from "./utils/constants";
import { authenticateUser, saveUserDetails } from "./utils/helper";

setup("authenticate as new user", async ({ page }) => {
await authenticateUser(page, userCredentials.NEW_USER_EMAIL);

await page.waitForURL("/create-workspace");
await expect(page).toHaveURL("/create-workspace");
await expect(page.locator("h3")).toContainText("Create a new workspace");

await page.context().storageState({ path: NEW_USER_FILE });
});

setup("authenticate as onboarded user", async ({ page }) => {
await authenticateUser(page, userCredentials.ONBOARDED_USER_EMAIL);

await page.waitForURL((url) => {
return (
ROUTES.PATTERNS.ONBOARDING_URL.test(url.pathname) ||
url.pathname === ROUTES.WORKSPACE.CREATE ||
ROUTES.PATTERNS.SURVEY_LIST_URL.test(url.pathname)
);
});

const currentUrl = page.url();

if (currentUrl.includes(ROUTES.WORKSPACE.CREATE)) {
await expect(page).toHaveURL(ROUTES.WORKSPACE.CREATE);
await expect(page.locator("h3")).toContainText("Create a new workspace");

await page.locator('[name="workspaceName"]').fill("onboarded");
await page.locator('[name="workspaceUrl"]').fill("onboarded");
await page.getByRole("button", { name: /Create Workspace/i }).click();

await page.waitForURL(ROUTES.PATTERNS.ONBOARDING_URL);
await handleOnboardingSteps(page);
await saveUserDetails(page, ONBOARDED_USER_FILE);
} else if (ROUTES.PATTERNS.ONBOARDING_URL.test(currentUrl)) {
await handleOnboardingSteps(page);
await saveUserDetails(page, ONBOARDED_USER_FILE);
} else if (ROUTES.PATTERNS.SURVEY_LIST_URL.test(currentUrl)) {
await saveUserDetails(page, ONBOARDED_USER_FILE);
} else {
throw new Error(`Unexpected URL: ${currentUrl}`);
}
});

setup("authenticate as non onboarded user", async ({ page }) => {
await authenticateUser(page, userCredentials.NON_ONBOARDED_USER_EMAIL);

await page.waitForURL((url) => {
return ROUTES.PATTERNS.ONBOARDING_URL.test(url.pathname) || url.pathname === ROUTES.WORKSPACE.CREATE;
});

const currentUrl = page.url();

if (currentUrl.includes(ROUTES.WORKSPACE.CREATE)) {
await expect(page).toHaveURL(ROUTES.WORKSPACE.CREATE);
await expect(page.locator("h3")).toContainText("Create a new workspace");

await page.locator('[name="workspaceName"]').fill(faker.word.words(2));
await page.locator('[name="workspaceUrl"]').fill(`${faker.word.words(1)}-${faker.word.words(1)}`);
await page.getByRole("button", { name: /Create Workspace/i }).click();

await page.waitForURL(ROUTES.PATTERNS.ONBOARDING_URL);
await saveUserDetails(page, NON_ONBOARDED_USER_FILE);
} else if (ROUTES.PATTERNS.ONBOARDING_URL.test(currentUrl)) {
await saveUserDetails(page, NON_ONBOARDED_USER_FILE);
} else {
throw new Error(`Unexpected URL: ${currentUrl}`);
}
});

async function handleOnboardingSteps(page: Page) {
const activeButton = page.locator('button[data-state="active"]');

const steps = [
/integrate sdk/i,
/identify your users/i,
/track your events/i,
/publish your first survey/i,
/connect your first integration/i,
];

for (const step of steps) {
await expect(activeButton).toContainText(step);
await page.getByRole("button", { name: /skip/i }).click();
}

await page.waitForURL((url) => ROUTES.PATTERNS.SURVEY_LIST_URL.test(url.pathname));
}
8 changes: 0 additions & 8 deletions apps/frontend/tests/example.spec.ts

This file was deleted.

4 changes: 4 additions & 0 deletions apps/frontend/tests/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UserDetails = {
workspaceSlug: string;
projectSlug: string;
};
32 changes: 32 additions & 0 deletions apps/frontend/tests/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const NEW_USER_FILE = "playwright/.auth/newUser.json";
export const ONBOARDED_USER_FILE = "playwright/.auth/onboardedUser.json";
export const NON_ONBOARDED_USER_FILE = "playwright/.auth/nonOnboardedUser.json";
export const userDetailsFile = "playwright/.auth/userDetails.json";

export const e2eTestToken = "e2e_test_token";

export const userCredentials = {
ONBOARDED_USER_EMAIL: "onboarded@example.com",
NEW_USER_EMAIL: "new-user@example.com",
NON_ONBOARDED_USER_EMAIL: "nonboarded@example.com",
};

export const ROUTES = {
WORKSPACE: {
CREATE: "/create-workspace",
ONBOARDING: (workspaceSlug: string, projectSlug: string) =>
`/${workspaceSlug}/projects/${projectSlug}/get-started`,
},

SURVEY: {
LIST: (workspaceSlug: string, projectSlug: string) => `/${workspaceSlug}/projects/${projectSlug}/surveys`,
SINGLE: (workspaceSlug: string, projectSlug: string, surveySlug: string) =>
`/${workspaceSlug}/projects/${projectSlug}/survey/${surveySlug}`,
},

PATTERNS: {
ONBOARDING_URL: /\/[\w-]+\/projects\/[\w-]+\/get-started/,
SURVEY_LIST_URL: /\/[\w-]+\/projects\/[\w-]+\/surveys/,
SINGLE_SURVEY: /\/[\w-]+\/projects\/[\w-]+\/survey\/[\w-]+/,
},
};
63 changes: 63 additions & 0 deletions apps/frontend/tests/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Page } from "@playwright/test";
import fs from "fs";
import { e2eTestToken, ROUTES, userDetailsFile } from "./constants";

export const waitForResponse = async (page: Page, operationName: string, actionCallback: () => void) => {
const [response] = await Promise.all([
page.waitForResponse((response) => {
const request = response.request();
if (request) {
const postData = request.postData();
if (postData && postData.includes(operationName)) {
return response.url().includes("/graphql") && response.status() === 200;
}
}
return false;
}),
actionCallback(),
]);

const request = response.request();
const postData = request?.postData();

if (postData) {
try {
const parsedData = JSON.parse(postData);
return parsedData;
} catch (error) {
return null;
}
}
return null;
};

export async function authenticateUser(page: Page, email: string) {
await page.goto("/");
await page.getByPlaceholder("Enter your email").fill(email);
await page.getByRole("button", { name: /Continue with email/i }).click();
await page.waitForURL(`/auth/magic-sign-in/?email=${encodeURIComponent(email)}`);
await page.getByRole("button", { name: /enter code manually/i }).click();
await page.fill('input[name="code"]', e2eTestToken);
await page.getByRole("button", { name: /continue/i }).click();
await page.waitForURL((url) => {
return (
ROUTES.PATTERNS.ONBOARDING_URL.test(url.pathname) ||
url.pathname === ROUTES.WORKSPACE.CREATE ||
ROUTES.PATTERNS.SURVEY_LIST_URL.test(url.pathname)
);
});
}

export function extractWorkspaceAndProjectSlugs(url: string) {
const pathname = new URL(url).pathname;
const workspaceSlug = pathname.split("/")[1];
const projectSlug = pathname.split("/")[3];
return { workspaceSlug, projectSlug };
}

export async function saveUserDetails(page: Page, storageFile: string) {
const { workspaceSlug, projectSlug } = extractWorkspaceAndProjectSlugs(page.url());
const details = { workspaceSlug, projectSlug };
fs.writeFileSync(userDetailsFile, JSON.stringify(details), "utf-8");
await page.context().storageState({ path: storageFile });
}
10 changes: 10 additions & 0 deletions apps/frontend/tests/workspace/create-workspace.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test } from "@playwright/test";
import { ONBOARDED_USER_FILE } from "../utils/constants";

test.describe("Create workspace", () => {
test.use({ storageState: ONBOARDED_USER_FILE });

test("should allow new user to create a new workspace", async ({ page }) => {
await page.goto("/create-workspace");
});
});
Empty file.
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,11 @@
dependencies:
"@f/map-obj" "^1.2.2"

"@faker-js/faker@^9.0.1":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.0.1.tgz#5e201ffc4524d00a200c648d2be55be6e25b3c3e"
integrity sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==

"@floating-ui/core@^1.0.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
Expand Down