password-generator.spec.ts

20.8 KB · raw

1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");1011async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {12  const start = Date.now();13  let sw = ctx.serviceWorkers()[0];14  while (!sw && Date.now() - start < maxWait) {15    await new Promise(r => setTimeout(r, 500));16    sw = ctx.serviceWorkers()[0];17  }18  if (!sw) throw new Error(`No SW after ${maxWait}ms`);19  return new URL(sw.url()).host;20}2122async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> {23  const extId = await waitForSW(ctx);24  const popup = await ctx.newPage();25  await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);26  await popup.waitForTimeout(3000);27  return { popup, extId };28}2930async function makeContext() {31  const { chromium } = await import("playwright");32  const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`);33  await fs.mkdir(userDataDir, { recursive: true });34  return chromium.launchPersistentContext(userDataDir, {35    headless: false,36    args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],37  });38}3940async function setupVault(ctx: BrowserContext, password = "password123") {41  const { popup } = await loadPopup(ctx);42  const pw = popup.locator("#master-password");43  if (await pw.isVisible()) {44    await pw.fill(password);45    await popup.locator("#confirm-password").fill(password);46    await popup.getByRole("button", { name: /create vault/i }).click();47    await popup.waitForTimeout(2000);48  }49  return { popup };50}5152test.describe("Password Generator", () => {53  let ctx: BrowserContext;54  test.afterEach(async () => { await ctx.close(); });55  test.beforeEach(async () => { ctx = await makeContext(); });5657  test("PG01 - generator button is visible", async () => {58    const { popup } = await setupVault(ctx);59    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator");60    await expect(genBtn.first()).toBeVisible({ timeout: 2000 });61    await ctx.close();62  });6364  test("PG02 - generator opens on click", async () => {65    const { popup } = await setupVault(ctx);66    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();67    await genBtn.click();68    await popup.waitForTimeout(300);69    const generated = popup.locator("[class*='generated'], [class*='password']").first();70    const hasGen = await generated.isVisible({ timeout: 2000 }).catch(() => false);71    expect(hasGen).toBeTruthy();72    await ctx.close();73  });7475  test("PG03 - generated password is not empty", async () => {76    const { popup } = await setupVault(ctx);77    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();78    await genBtn.click();79    await popup.waitForTimeout(500);80    const password = popup.locator("[class*='generated-password'], [class*='password-output']").first();81    const text = (await password.textContent().catch(() => "")) ?? "";82    expect(text.length).toBeGreaterThan(0);83    await ctx.close();84  });8586  test("PG04 - regenerate button creates new password", async () => {87    const { popup } = await setupVault(ctx);88    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();89    await genBtn.click();90    await popup.waitForTimeout(500);91    const pass1 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";92    const regenBtn = popup.locator("text=regenerate, [aria-label*='regenerate' i]").first();93    await regenBtn.click();94    await popup.waitForTimeout(300);95    const pass2 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";96    expect(pass2).not.toEqual(pass1);97    await ctx.close();98  });99100  test("PG05 - copy button copies password", async () => {101    const { popup } = await setupVault(ctx);102    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();103    await genBtn.click();104    await popup.waitForTimeout(500);105    const copyBtn = popup.locator("[aria-label*='copy' i], [aria-label*='Copy']").first();106    await copyBtn.click();107    await popup.waitForTimeout(300);108    const copied = popup.locator("text=/copied|\\u2713/i");109    const hasCopied = await copied.isVisible({ timeout: 2000 }).catch(() => false);110    expect(hasCopied).toBeTruthy();111    await ctx.close();112  });113114  test("PG06 - password length slider", async () => {115    const { popup } = await setupVault(ctx);116    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();117    await genBtn.click();118    await popup.waitForTimeout(500);119    const lengthSlider = popup.locator("input[type='range'], [class*='length']").first();120    const hasSlider = await lengthSlider.isVisible().catch(() => false);121    expect(hasSlider).toBeTruthy();122    await ctx.close();123  });124125  test("PG07 - uppercase toggle", async () => {126    const { popup } = await setupVault(ctx);127    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();128    await genBtn.click();129    await popup.waitForTimeout(500);130    const uppercaseToggle = popup.locator("text=/uppercase|A-Z/i").first();131    const hasToggle = await uppercaseToggle.isVisible().catch(() => false);132    expect(hasToggle).toBeTruthy();133    await ctx.close();134  });135136  test("PG08 - lowercase toggle", async () => {137    const { popup } = await setupVault(ctx);138    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();139    await genBtn.click();140    await popup.waitForTimeout(500);141    const lowercaseToggle = popup.locator("text=/lowercase|a-z/i").first();142    const hasToggle = await lowercaseToggle.isVisible().catch(() => false);143    expect(hasToggle).toBeTruthy();144    await ctx.close();145  });146147  test("PG09 - numbers toggle", async () => {148    const { popup } = await setupVault(ctx);149    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();150    await genBtn.click();151    await popup.waitForTimeout(500);152    const numbersToggle = popup.locator("text=/numbers|0-9/i").first();153    const hasToggle = await numbersToggle.isVisible().catch(() => false);154    expect(hasToggle).toBeTruthy();155    await ctx.close();156  });157158  test("PG10 - symbols toggle", async () => {159    const { popup } = await setupVault(ctx);160    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();161    await genBtn.click();162    await popup.waitForTimeout(500);163    const symbolsToggle = popup.locator("text=/symbols|!@#$/i").first();164    const hasToggle = await symbolsToggle.isVisible().catch(() => false);165    expect(hasToggle).toBeTruthy();166    await ctx.close();167  });168169  test("PG11 - generated password contains uppercase when enabled", async () => {170    const { popup } = await setupVault(ctx);171    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();172    await genBtn.click();173    await popup.waitForTimeout(500);174    const uppercaseToggle = popup.locator("text=/uppercase|A-Z/i]").first();175    const isEnabled = await uppercaseToggle.isChecked().catch(() => true);176    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";177    if (isEnabled) expect(password).toMatch(/[A-Z]/);178    await ctx.close();179  });180181  test("PG12 - generated password contains lowercase when enabled", async () => {182    const { popup } = await setupVault(ctx);183    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();184    await genBtn.click();185    await popup.waitForTimeout(500);186    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";187    expect(password).toMatch(/[a-z]/);188    await ctx.close();189  });190191  test("PG13 - generated password contains numbers when enabled", async () => {192    const { popup } = await setupVault(ctx);193    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();194    await genBtn.click();195    await popup.waitForTimeout(500);196    const numbersToggle = popup.locator("text=/numbers|0-9/i").first();197    const isEnabled = await numbersToggle.isChecked().catch(() => true);198    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";199    if (isEnabled) expect(password).toMatch(/[0-9]/);200    await ctx.close();201  });202203  test("PG14 - generated password contains symbols when enabled", async () => {204    const { popup } = await setupVault(ctx);205    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();206    await genBtn.click();207    await popup.waitForTimeout(500);208    const symbolsToggle = popup.locator("text=/symbols|!@#$/i").first();209    const isEnabled = await symbolsToggle.isChecked().catch(() => true);210    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";211    if (isEnabled) expect(password).toMatch(/[!@#$%^&*()_+\-=\[\]{}|;':\",.\/<>?]/);212    await ctx.close();213  });214215  test("PG15 - password strength indicator", async () => {216    const { popup } = await setupVault(ctx);217    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();218    await genBtn.click();219    await popup.waitForTimeout(500);220    const strength = popup.locator("[class*='strength'], [class*='weak'], [class*='strong']").first();221    const hasStrength = await strength.isVisible().catch(() => false);222    expect(hasStrength).toBeTruthy();223    await ctx.close();224  });225226  test("PG16 - generator has keyboard shortcut", async () => {227    const { popup } = await setupVault(ctx);228    await popup.keyboard.press("Control+Shift+G");229    await popup.waitForTimeout(300);230    const gen = popup.locator("[class*='generator'], [class*='password']").first();231    const hasGen = await gen.isVisible({ timeout: 2000 }).catch(() => false);232    expect(hasGen).toBeTruthy();233    await ctx.close();234  });235236  test("PG17 - use password button", async () => {237    const { popup } = await setupVault(ctx);238    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();239    await genBtn.click();240    await popup.waitForTimeout(500);241    const useBtn = popup.locator("text=/use|apply/i").first();242    await useBtn.click();243    await popup.waitForTimeout(300);244    await ctx.close();245  });246247  test("PG18 - generator closes on escape", async () => {248    const { popup } = await setupVault(ctx);249    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();250    await genBtn.click();251    await popup.waitForTimeout(300);252    await popup.keyboard.press("Escape");253    await popup.waitForTimeout(200);254    await ctx.close();255  });256257  test("PG19 - minimum length is enforced", async () => {258    const { popup } = await setupVault(ctx);259    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();260    await genBtn.click();261    await popup.waitForTimeout(500);262    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";263    expect(password.length).toBeGreaterThanOrEqual(8);264    await ctx.close();265  });266267  test("PG20 - maximum length is reasonable", async () => {268    const { popup } = await setupVault(ctx);269    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();270    await genBtn.click();271    await popup.waitForTimeout(500);272    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";273    expect(password.length).toBeLessThanOrEqual(128);274    await ctx.close();275  });276});277278test.describe("Password Generator - Advanced", () => {279  let ctx: BrowserContext;280  test.afterEach(async () => { await ctx.close(); });281  test.beforeEach(async () => { ctx = await makeContext(); });282283  test("PGA01 - passphrase generation option", async () => {284    const { popup } = await setupVault(ctx);285    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();286    await genBtn.click();287    await popup.waitForTimeout(500);288    const passphraseOpt = popup.locator("text=/passphrase|words/i").first();289    const hasPassphrase = await passphraseOpt.isVisible().catch(() => false);290    expect(hasPassphrase).toBeTruthy();291    await ctx.close();292  });293294  test("PGA02 - word count for passphrases", async () => {295    const { popup } = await setupVault(ctx);296    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();297    await genBtn.click();298    await popup.waitForTimeout(500);299    const passphraseOpt = popup.locator("text=/passphrase|words/i").first();300    const hasOpt = await passphraseOpt.isVisible().catch(() => false);301    if (hasOpt) {302      await passphraseOpt.click();303      await popup.waitForTimeout(300);304    }305    const wordCount = popup.locator("input[type='number'], [class*='word']").first();306    const hasWordCount = await wordCount.isVisible().catch(() => false);307    expect(hasWordCount).toBeTruthy();308    await ctx.close();309  });310311  test("PGA03 - exclude ambiguous characters option", async () => {312    const { popup } = await setupVault(ctx);313    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();314    await genBtn.click();315    await popup.waitForTimeout(500);316    const excludeOpt = popup.locator("text=/exclude|ambiguous/i").first();317    const hasExclude = await excludeOpt.isVisible().catch(() => false);318    expect(hasExclude).toBeTruthy();319    await ctx.close();320  });321322  test("PGA04 - history of generated passwords", async () => {323    const { popup } = await setupVault(ctx);324    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();325    await genBtn.click();326    await popup.waitForTimeout(500);327    await popup.locator("text=regenerate, [aria-label*='regenerate' i]").first().click();328    await popup.waitForTimeout(300);329    const history = popup.locator("text=/history|recent/i").first();330    const hasHistory = await history.isVisible().catch(() => false);331    if (hasHistory) expect(true).toBeTruthy();332    else expect(hasHistory).toBeFalsy();333    await ctx.close();334  });335336  test("PGA05 - custom character exclusions", async () => {337    const { popup } = await setupVault(ctx);338    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();339    await genBtn.click();340    await popup.waitForTimeout(500);341    const customExclude = popup.locator("text=/custom|exclude/i").first();342    const hasCustom = await customExclude.isVisible().catch(() => false);343    expect(hasCustom).toBeTruthy();344    await ctx.close();345  });346347  test("PGA06 - pronounceable password option", async () => {348    const { popup } = await setupVault(ctx);349    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();350    await genBtn.click();351    await popup.waitForTimeout(500);352    const pronounceable = popup.locator("text=/pronounceable|readable/i").first();353    const hasPronounceable = await pronounceable.isVisible().catch(() => false);354    expect(hasPronounceable).toBeTruthy();355    await ctx.close();356  });357358  test("PGA07 - pin-only generation option", async () => {359    const { popup } = await setupVault(ctx);360    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();361    await genBtn.click();362    await popup.waitForTimeout(500);363    const pinOpt = popup.locator("text=/pin|digit/i").first();364    const hasPin = await pinOpt.isVisible().catch(() => false);365    expect(hasPin).toBeTruthy();366    await ctx.close();367  });368369  test("PGA08 - generated password entropy is sufficient", async () => {370    const { popup } = await setupVault(ctx);371    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();372    await genBtn.click();373    await popup.waitForTimeout(500);374    const password = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";375    expect(password.length).toBeGreaterThanOrEqual(12);376    await ctx.close();377  });378});379380test.describe("Password Generator - Entry Integration", () => {381  let ctx: BrowserContext;382  test.afterEach(async () => { await ctx.close(); });383  test.beforeEach(async () => { ctx = await makeContext(); });384385  test("PGI01 - generator works in new entry form", async () => {386    const { popup } = await setupVault(ctx);387    await popup.locator('input[placeholder="search vault..."]').click();388    await popup.getByRole("button", { name: /new entry/i }).click();389    await popup.waitForTimeout(300);390    const genBtn = popup.locator("[aria-label*='generate' i]").first();391    await genBtn.click();392    await popup.waitForTimeout(500);393    const generated = popup.locator("[class*='generated'], [class*='password']").first();394    const hasGen = await generated.isVisible({ timeout: 2000 }).catch(() => false);395    expect(hasGen).toBeTruthy();396    await ctx.close();397  });398399  test("PGI02 - generated password fills entry password field", async () => {400    const { popup } = await setupVault(ctx);401    await popup.locator('input[placeholder="search vault..."]').click();402    await popup.getByRole("button", { name: /new entry/i }).click();403    await popup.waitForTimeout(300);404    const genBtn = popup.locator("[aria-label*='generate' i]").first();405    await genBtn.click();406    await popup.waitForTimeout(500);407    const useBtn = popup.locator("text=/use|apply/i").first();408    await useBtn.click();409    await popup.waitForTimeout(300);410    const passwordField = popup.locator(".peach-new-field-input[placeholder='password']");411    const hasPassword = await passwordField.inputValue().catch(() => "") ?? "";412    expect(hasPassword.length).toBeGreaterThan(0);413    await ctx.close();414  });415416  test("PGI03 - generator accessible from entry detail", async () => {417    const { popup } = await setupVault(ctx);418    await popup.locator('input[placeholder="search vault..."]').click();419    await popup.getByRole("button", { name: /new entry/i }).click();420    await popup.locator(".peach-new-name-input").fill("GenTest");421    await popup.locator('input[placeholder="domain.com"]').fill("https://gentest.com");422    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");423    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");424    await popup.getByRole("button", { name: /create/i }).click();425    await popup.waitForTimeout(1500);426    await popup.locator(".peach-entry-name-button").first().click();427    await popup.waitForTimeout(300);428    const genBtn = popup.locator("[aria-label*='generate' i]").first();429    const hasGen = await genBtn.isVisible().catch(() => false);430    expect(hasGen).toBeTruthy();431    await ctx.close();432  });433434  test("PGI04 - regenerate updates entry password", async () => {435    const { popup } = await setupVault(ctx);436    await popup.locator('input[placeholder="search vault..."]').click();437    await popup.getByRole("button", { name: /new entry/i }).click();438    await popup.waitForTimeout(300);439    const genBtn = popup.locator("[aria-label*='generate' i]").first();440    await genBtn.click();441    await popup.waitForTimeout(500);442    const pass1 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";443    const regenBtn = popup.locator("text=regenerate").first();444    await regenBtn.click();445    await popup.waitForTimeout(300);446    const pass2 = await popup.locator("[class*='generated-password'], [class*='password-output']").first().textContent().catch(() => "") ?? "";447    expect(pass1).not.toEqual(pass2);448    await ctx.close();449  });450451  test("PGI05 - password copied notification", async () => {452    const { popup } = await setupVault(ctx);453    const genBtn = popup.locator("[aria-label*='generate' i], text=generate, text=generator").first();454    await genBtn.click();455    await popup.waitForTimeout(500);456    const copyBtn = popup.locator("[aria-label*='copy' i]").first();457    await copyBtn.click();458    await popup.waitForTimeout(300);459    const notification = popup.locator("text=/copied|\\u2713|copied to clipboard/i");460    const hasNotification = await notification.isVisible({ timeout: 2000 }).catch(() => false);461    expect(hasNotification).toBeTruthy();462    await ctx.close();463  });464});