Skip to content
Draft
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
39 changes: 39 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
working-directory: browser

- name: Run Playwright tests (excluding extension tests that require build)
run: pnpm test --grep-invert extension
working-directory: browser

- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: browser/playwright-report/
retention-days: 30
Comment on lines +9 to +39

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 4 months ago

To fix this problem, add a permissions section with least-privilege settings. In this workflow, the only needed permission is likely contents: read, which grants read access to repository contents, enabling actions like checkout and reading code, but preventing pushes or sensitive changes. This can be added either at the root (applies to all jobs) or at the test job level. Since there is only one job, adding at the root is clear and future-proof.

Change required:

  • At the top (after the name field), insert:
    permissions:
      contents: read
  • No new methods, definitions, or imports are needed, as this is a static configuration in YAML.

Suggested changeset 1
.github/workflows/playwright.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -1,4 +1,6 @@
 name: Playwright Tests
+permissions:
+  contents: read
 on:
   push:
     branches: [ main ]
EOF
@@ -1,4 +1,6 @@
name: Playwright Tests
permissions:
contents: read
on:
push:
branches: [ main ]
Copilot is powered by AI and may make mistakes. Always verify output.
9 changes: 9 additions & 0 deletions browser/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ dist-ssr
*.sln
*.sw?
build
.next
out
dist

# Playwright
/playwright-report
/test-results
/blob-report
/playwright/.cache
3 changes: 3 additions & 0 deletions browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"start:firefox": "web-ext run --start-url https://mozilla.org --browser-console --source-dir build",
"build": "tsc -b && vite build",
"lint": "eslint .",
"test": "playwright test",
"preview": "vite preview"
},
"dependencies": {
Expand All @@ -28,6 +29,8 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@playwright/test": "^1.57.0",
"@types/node": "^24.10.1",
"@types/qrcode": "^1.5.6",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
Expand Down
47 changes: 47 additions & 0 deletions browser/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { defineConfig, devices } from '@playwright/test';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

Check warning on line 7 in browser/playwright.config.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=clFaster_TapToQR&issues=AZsTxq5rmPgnbCdmjbsi&open=AZsTxq5rmPgnbCdmjbsi&pullRequest=55

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* 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,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* 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. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},

/* Configure projects */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
}
],

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});
32 changes: 14 additions & 18 deletions browser/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const Popup = () => {
const [qrCodeSvg, setQrCodeSvg] = useState("");

const generateQrContent = async (extensionSettings: ExtensionSettings) => {
const currentUrl = (await getActiveTab()) || "hallo";
const currentUrl = await getActiveTab();
const svg = await generateSvgContent(
currentUrl,
extensionSettings.qrCodeSize,
Expand All @@ -39,42 +39,38 @@ const Popup = () => {

useEffect(() => {
loadExtensionSettings().then((extensionSettings) => {
generateQrContent(extensionSettings).then(async () => {
generateQrContent(extensionSettings).then(() => {
const newWidth = extensionSettings.qrCodeSize + 10;
const popupContainer = document.getElementById("popup-container");
popupContainer?.setAttribute("style", `width: ${newWidth}px`);
});
});
}, []);

const generateQrSvg = async (size: number) => {
const extensionSettings = await loadExtensionSettings();
const currentUrl = await getActiveTab();
return generateSvgContent(currentUrl, size, extensionSettings.displayLogo);
};

const copyToClipboard = async () => {
const extensionSettings = await loadExtensionSettings();
const currentUrl = (await getActiveTab()) || "hallo";
const svg = await generateSvgContent(
currentUrl,
extensionSettings.qrCodeDownloadSize,
extensionSettings.displayLogo,
);
copyQrCodeToClipboard(svg, extensionSettings.qrCodeDownloadSize).then();
const svg = await generateQrSvg(extensionSettings.qrCodeDownloadSize);
await copyQrCodeToClipboard(svg, extensionSettings.qrCodeDownloadSize);
};

const downloadQrCode = async () => {
const extensionSettings = await loadExtensionSettings();
const currentUrl = (await getActiveTab()) || "hallo";
const svg = await generateSvgContent(
currentUrl,
extensionSettings.qrCodeDownloadSize,
extensionSettings.displayLogo,
);
downloadQrCodeAsPng(svg, extensionSettings.qrCodeDownloadSize).then();
const svg = await generateQrSvg(extensionSettings.qrCodeDownloadSize);
await downloadQrCodeAsPng(svg, extensionSettings.qrCodeDownloadSize);
};

const openCustomQrCodeWindow = () => {
openCustomQrPage();
};

const openExtensionSettings = () => {
openExtensionSettingsPage().then();
const openExtensionSettings = async () => {
await openExtensionSettingsPage();
};

return (
Expand Down
13 changes: 11 additions & 2 deletions browser/src/utils/browser-tabs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import browser from "webextension-polyfill";

export const getActiveTab = async () => {
export const getActiveTab = async (): Promise<string> => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return tabs[0].url;
const url = tabs[0]?.url;

// Return URL if found, otherwise check for test fallback
if (url) {
return url;
}

// Fallback for testing when no active tab is available
const urlParams = new URLSearchParams(window.location.search);

Check warning on line 13 in browser/src/utils/browser-tabs.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=clFaster_TapToQR&issues=AZsTxq3bmPgnbCdmjbsh&open=AZsTxq3bmPgnbCdmjbsh&pullRequest=55
return urlParams.get('url') || "URL not found";
};
125 changes: 125 additions & 0 deletions browser/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# TapToQR Browser Extension Tests

This directory contains Playwright tests for the TapToQR browser extension.

## Test Structure

- **example.spec.ts** - Basic smoke tests to verify test environment
- **qr-data-formatters.spec.ts** - Unit tests for QR data formatting functions (WiFi, Contact, Email, etc.)
- **qr-generation.spec.ts** - Integration tests for QR code generation library
- **extension.spec.ts** - End-to-end UI tests for the extension (requires build)
- **fixtures.ts** - Custom Playwright fixtures for extension testing

## Running Tests

### All Tests (Unit + Integration, excluding extension tests)
```bash
pnpm test --grep-invert extension
```

### All Tests Including Extension Tests
```bash
# Build the extension first
pnpm build

# Run all tests
pnpm test --workers=1
```

### Specific Test File
```bash
pnpm exec playwright test qr-data-formatters.spec.ts
```

### With UI Mode (Interactive)
```bash
pnpm exec playwright test --ui
```

### Extension Tests Only
```bash
# Build the extension first
pnpm build

# Run extension tests
pnpm test extension.spec.ts --workers=1
```

**Note:** Extension tests require:
1. The extension to be built first (`pnpm build`)
2. Single worker mode (`--workers=1`) to avoid conflicts with browser contexts
3. A background service worker in the manifest to get the extension ID

## Setup

Install Playwright browsers (one-time setup):
```bash
pnpm exec playwright install
```

## Test Categories

### Unit Tests
Tests that don't require a browser or extension context:
- Data formatters (19 tests)
- Utility functions

### Integration Tests
Tests that use the browser but not the extension:
- QR code generation (7 tests)
- Library integration

### E2E/UI Tests
Tests that require the built extension (17 tests per project):
- Extension popup UI (buttons, QR code display, navigation)
- Extension options page (settings, sliders, checkboxes, save/revert)
- Custom QR page loading
- Extension manifest validation

## CI/CD

Tests run automatically on:
- Pull requests to main/master
- Pushes to main/master

The CI workflow excludes extension tests that require the built extension.

See `.github/workflows/playwright.yml` for CI configuration.

## Writing Tests

### For Unit Tests
Use standard Playwright test syntax:
```typescript
import { test, expect } from '@playwright/test';

test('my test', () => {
// test code
});
```

### For Extension Tests
Use the custom fixtures that provide the extension context:
```typescript
import { test, expect } from './fixtures';

test('extension test', async ({ page, extensionId }) => {
await page.goto(`chrome-extension://${extensionId}/pages/popup.html`);
await page.waitForLoadState('networkidle');

// Test interactions with the extension UI
await expect(page.locator('#some-element')).toBeVisible();
});
```

## Extension Testing Approach

Based on [Playwright's Chrome Extensions documentation](https://playwright.dev/docs/chrome-extensions), our tests:

1. Launch a persistent browser context with the extension loaded
2. Use a background service worker to get the extension ID
3. Navigate to extension pages using `chrome-extension://<id>/pages/...` URLs
4. Test UI interactions, form submissions, and navigation
5. Verify manifest properties and extension configuration

The extension must have a background service worker defined in the manifest for the tests to work properly.
37 changes: 37 additions & 0 deletions browser/tests/extension.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect } from './fixtures';

test.describe('Extension Popup UI', () => {
test('should load and display QR code with correct URL', async ({ page, extensionId, context }) => {
// Navigate to a test page first
const testUrl = 'https://moritzreis.dev';

// Open the popup with the URL as a query parameter
// This simulates what would happen if the popup had access to the active tab
const popupPage = await context.newPage();
await popupPage.goto(
`chrome-extension://${extensionId}/pages/popup.html?url=${encodeURIComponent(testUrl)}`
);
await popupPage.waitForLoadState('networkidle');

// Check that the popup container is visible
await expect(popupPage.locator('#popup-container')).toBeVisible();

// Check that the header is present
await expect(popupPage.getByText('TapToQR')).toBeVisible();

// Check that the QR code SVG is rendered
const qrCodeContainer = popupPage.locator('svg').first();
await expect(qrCodeContainer).toBeVisible();

// Verify the QR code was generated (check that there are path elements)
const paths = popupPage.locator('svg path');
await expect(paths.first()).toBeVisible();

// Optional: You could decode the QR code to verify it contains the correct URL
// For now, we verify that the QR code rendered and the correct URL was passed
const currentUrl = popupPage.url();
expect(currentUrl).toContain(encodeURIComponent(testUrl));

await popupPage.close();
});
});
Loading
Loading