Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"Bash(bash:*)",
"Bash(npm test:*)",
"Bash(npm run test:e2e:*)",
"Bash(npm run test:cucumber:*)"
"Bash(npm run test:cucumber:*)",
"Bash(open /Users/ian/projects/my/save-markdown/reports/cucumber_report.html)"
],
"deny": []
}
Expand Down
149 changes: 148 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ name: CI
on:
push:

# Permissions needed for GitHub Pages deployment
permissions:
contents: read
pages: write
id-token: write

jobs:
test:
name: Build and Test
Expand Down Expand Up @@ -54,12 +60,13 @@ jobs:
run: |
mkdir -p ./target/e2e-downloads
mkdir -p ./test-results
mkdir -p ./reports

- name: Run e2e tests
run: npm run test:e2e

- name: Run cucumber e2e tests
run: npm run test:cucumber
run: npm run test:cucumber:report

- name: Check test results
if: always()
Expand All @@ -71,6 +78,21 @@ jobs:
echo "No test results file found"
fi

- name: Check cucumber reports
if: always()
run: |
if [ -f "reports/cucumber_report.html" ]; then
echo "✅ Cucumber HTML report generated"
ls -la reports/
else
echo "❌ Cucumber HTML report not found"
fi
if [ -f "reports/cucumber_report.json" ]; then
echo "✅ Cucumber JSON report generated"
else
echo "❌ Cucumber JSON report not found"
fi

- name: Upload test reports
uses: actions/upload-artifact@v4
if: always()
Expand All @@ -86,3 +108,128 @@ jobs:
name: playwright-test-results
path: test-results/
retention-days: 30

- name: Upload cucumber reports
uses: actions/upload-artifact@v4
if: always()
with:
name: cucumber-reports
path: reports/
retention-days: 30

- name: Create GitHub Pages site structure
if: always()
run: |
mkdir -p gh-pages
cp -r reports/* gh-pages/ 2>/dev/null || true

# Create an index.html that redirects to the cucumber report
cat > gh-pages/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>Test Reports - Save Markdown Extension</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
line-height: 1.6;
}
.header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.report-links {
display: grid;
gap: 1rem;
margin-top: 2rem;
}
.report-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
background: #f9f9f9;
transition: transform 0.2s;
}
.report-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.report-card h3 {
margin-top: 0;
color: #333;
}
.report-card p {
color: #666;
margin-bottom: 1rem;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
background: #0066cc;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.2s;
}
.btn:hover {
background: #0052a3;
}
.timestamp {
color: #888;
font-size: 0.9em;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="header">
<h1>🧪 Test Reports</h1>
<h2>Save Markdown Extension</h2>
<p>Automated test results from CI/CD pipeline</p>
</div>

<div class="report-links">
<div class="report-card">
<h3>🥒 Cucumber E2E Test Report</h3>
<p>Interactive HTML report with screenshots showing the complete extension workflow from creating save rules to saving markdown files.</p>
<a href="cucumber_report.html" class="btn">View Cucumber Report</a>
</div>

<div class="report-card">
<h3>📊 Cucumber JSON Data</h3>
<p>Machine-readable test results in JSON format for further processing or integration with other tools.</p>
<a href="cucumber_report.json" class="btn">View JSON Data</a>
</div>
</div>

<div class="timestamp">
<p>Generated: <span id="timestamp"></span></p>
<p>Commit: <span id="commit">$GITHUB_SHA</span></p>
</div>

<script>
document.getElementById('timestamp').textContent = new Date().toLocaleString();
document.getElementById('commit').textContent = '$GITHUB_SHA'.substring(0, 7);
</script>
</body>
</html>
EOF

# Replace placeholders with actual values
sed -i.bak "s/\$GITHUB_SHA/$GITHUB_SHA/g" gh-pages/index.html && rm gh-pages/index.html.bak

- name: Deploy to GitHub Pages
if: always() && github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./gh-pages
destination_dir: reports
keep_files: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.vite/
coverage/
dist/
gh-pages/
node_modules/
playwright-report/
release/
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fi
npx lint-staged

# Build and test
npm run lint
npm run build
npm test
npm run test:e2e
Expand Down
6 changes: 5 additions & 1 deletion cucumber.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ module.exports = {
default: {
import: ['features/step_definitions/**/*.ts', 'features/support/**/*.ts'],
loader: ['ts-node/esm'],
format: ['progress', 'json:reports/cucumber_report.json'],
format: [
'progress',
'json:reports/cucumber_report.json',
'html:reports/cucumber_report.html',
],
formatOptions: {
snippetInterface: 'async-await',
},
Expand Down
59 changes: 34 additions & 25 deletions features/step_definitions/extension.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { chromium, BrowserContext, Page, devices } from '@playwright/test';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import fs from 'node:fs';
import { CustomWorld } from '../support/world';

setDefaultTimeout(60_000);

Expand Down Expand Up @@ -69,14 +70,18 @@ After({ tags: '@extension' }, async function () {
}
});

Given('I have the extension loaded in the browser', async function () {
// Extension is already loaded in the Before hook
assert(extensionId, 'Extension should be loaded');
});
Given(
'I have the extension loaded in the browser',
async function (this: CustomWorld) {
// Extension is already loaded in the Before hook
assert(extensionId, 'Extension should be loaded');
},
);

When('I open the extension options page', async function () {
When('I open the extension options page', async function (this: CustomWorld) {
const optionsUrl = `chrome-extension://${extensionId}/options.html`;
page = await context.newPage();
this.page = page; // Set page reference for screenshots
await page.goto(optionsUrl);

// Wait for options page to load
Expand All @@ -85,7 +90,7 @@ When('I open the extension options page', async function () {

When(
'I add a new suggested rule with the following details:',
async function (dataTable: DataTable) {
async function (this: CustomWorld, dataTable: DataTable) {
const data = dataTable.rowsHash();

// Click "Add Suggested Rule" button
Expand All @@ -109,18 +114,18 @@ When(
},
);

When('I enable the status window', async function () {
When('I enable the status window', async function (this: CustomWorld) {
// Enable status window using the toggle in options page
const statusWindowSelect = page.locator('#showStatusWindow');
await statusWindowSelect.selectOption('true');
});

When('I save the options', async function () {
When('I save the options', async function (this: CustomWorld) {
// Save the options to ensure the setting is persisted
await page.click('#saveOptions');
});

When('I navigate to the test page', async function () {
When('I navigate to the test page', async function (this: CustomWorld) {
// Close options page and navigate to test page
await page.close();

Expand All @@ -130,6 +135,7 @@ When('I navigate to the test page', async function () {
'../../tests/e2e/fixtures/test-page.html',
);
page = await context.newPage();
this.page = page; // Set page reference for screenshots
const session = await context.newCDPSession(page);
await session.send('Browser.setDownloadBehavior', {
downloadPath: downloadsPath,
Expand All @@ -139,33 +145,36 @@ When('I navigate to the test page', async function () {
await page.goto(`file://${testPagePath}`);
});

When('I click the save rule button', async function () {
When('I click the save rule button', async function (this: CustomWorld) {
// Look for the suggested save element and click "ADD SAVE RULE" to convert it to auto-save
const suggestedElement = page.locator('.add-save-rule-button').first();
await suggestedElement.click();
});

Then('the status window should be visible', async function () {
Then('the status window should be visible', async function (this: CustomWorld) {
// Check if the status window is visible on the page
const statusWindow = page.locator('#markdown-save-status-window');
const isVisible = await statusWindow.isVisible({ timeout: 5000 });
assert(isVisible, 'Status window should be visible');
});

Then('the status window should show a success message', async function () {
// Verify the status window contains a success message
const statusWindow = page.locator('#markdown-save-status-window');

// Wait for status item to appear with timeout
const statusItem = statusWindow.locator('div[role="status"]').first();
await statusItem.waitFor({ state: 'visible', timeout: 10000 });

// Check if status item contains expected elements
const filenameElement = statusItem.locator('.filename');
await filenameElement.waitFor({ state: 'visible', timeout: 5000 });
});
Then(
'the status window should show a success message',
async function (this: CustomWorld) {
// Verify the status window contains a success message
const statusWindow = page.locator('#markdown-save-status-window');

// Wait for status item to appear with timeout
const statusItem = statusWindow.locator('div[role="status"]').first();
await statusItem.waitFor({ state: 'visible', timeout: 10000 });

// Check if status item contains expected elements
const filenameElement = statusItem.locator('.filename');
await filenameElement.waitFor({ state: 'visible', timeout: 5000 });
},
);

Then('a markdown file should be created', async function () {
Then('a markdown file should be created', async function (this: CustomWorld) {
// Extract the filename from the status window for verification
const statusWindow = page.locator('#markdown-save-status-window');
const statusItem = statusWindow.locator('div[role="status"]').first();
Expand All @@ -185,7 +194,7 @@ Then('a markdown file should be created', async function () {

Then(
'the markdown content should contain:',
async function (dataTable: DataTable) {
async function (this: CustomWorld, dataTable: DataTable) {
assert(downloadedFilename, 'Downloaded filename should be available');

// Read the content of the downloaded file
Expand Down
15 changes: 15 additions & 0 deletions features/support/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AfterStep } from '@cucumber/cucumber';
import { CustomWorld } from './world';

AfterStep(async function (this: CustomWorld, { pickle, pickleStep }) {
// Only take screenshots for extension tests that have a page
const isExtensionTest = pickle.tags?.some(tag => tag.name === '@extension');

if (isExtensionTest && this.page) {
const stepName = pickleStep.text.replace(/[^a-zA-Z0-9]/g, '_');
const scenarioName = pickle.name.replace(/[^a-zA-Z0-9]/g, '_');
const screenshotName = `${scenarioName}_${stepName}`;

await this.attachScreenshot(this.page, screenshotName);
}
});
22 changes: 22 additions & 0 deletions features/support/world.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
/* eslint-disable no-console */
import { World, setWorldConstructor, IWorldOptions } from '@cucumber/cucumber';
import { Page } from '@playwright/test';

class CustomWorld extends World {
public page?: Page;

constructor(options: IWorldOptions) {
super(options);
}

async attachScreenshot(page: Page, name: string): Promise<void> {
if (page && !page.isClosed()) {
try {
const screenshot = await page.screenshot({
fullPage: true,
type: 'png',
});

this.attach(screenshot, {
mediaType: 'image/png',
fileName: `${name}.png`,
});
} catch (error) {
console.warn(`Failed to take screenshot for ${name}:`, error);
}
}
}
}

setWorldConstructor(CustomWorld);
Expand Down
Loading
Loading