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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ node_modules/
/playwright-report/
/blob-report/
/playwright/.cache/
.env
.env
.DS_Store
47 changes: 47 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
max-width: 600px;
}
h1 {
font-size: 4em;
margin: 0;
}
p {
font-size: 1.2em;
margin: 10px 0 20px;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<p>Oops! The page you are looking for does not exist.</p>
<p><a href="/">Go back to the homepage</a></p>
</div>
</body>
</html>
297 changes: 297 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Playwright Reports</title>
<style>
/* High-contrast charcoal theme (less blue) */
:root{
--bg:#0f1115; /* charcoal */
--panel:#12151c; /* panel bg */
--border:#2b313c; /* separators */
--fg:#e8eaf0; /* primary text */
--muted:#a6adbb; /* secondary text */
--link:#b5c7ff; /* desaturated link */
--link-hover:#d3deff; /* link hover */
--chip:#151922; /* chip bg */
--chipfg:#d6d9e0; /* chip text */
--chip-bd:#313846; /* chip border */
--ok-bg:#0f2417; --ok-bd:#2ea043; --ok-fg:#8ee6a8;
--err-bg:#281416; --err-bd:#f85149; --err-fg:#f2aaaa;
--warn-bg:#241d0f; --warn-bd:#d29922; --warn-fg:#f3d08c;
--row-hover:#141823; /* row hover */
--input-bg:#0f131a; --input-bd:#313846; --input-fg:#e8eaf0;
--focus:#6ea8ff; /* focus ring */
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0; padding:24px;
font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif;
background:var(--bg); color:var(--fg); line-height:1.5;
}
h1{margin:0 0 8px;font-weight:700;letter-spacing:.2px}
a{color:var(--link); text-decoration:none}
a:hover{color:var(--link-hover); text-decoration:underline}
a:focus-visible, button:focus-visible, select:focus-visible, input:focus-visible{
outline:2px solid var(--focus); outline-offset:2px; border-radius:6px;
}
.meta{color:var(--muted);margin:0 0 12px}

.toolbar{
display:flex; gap:10px; flex-wrap:wrap; align-items:center;
margin:16px 0 8px;
}

/* Inputs */
select,input[type="search"]{
background:var(--input-bg); color:var(--input-fg);
border:1px solid var(--input-bd); border-radius:10px;
padding:8px 12px; min-width:180px; outline:none;
}
input[type="search"]{min-width:300px}

/* Status pills */
.pills{display:flex; gap:6px; flex-wrap:wrap}
.pill{
background:var(--chip); color:var(--chipfg);
border:1px solid var(--chip-bd); border-radius:999px;
padding:5px 12px; font-size:.9em; cursor:pointer; user-select:none;
transition:background .15s ease,border-color .15s ease,color .15s ease;
}
.pill:hover{border-color:var(--link)}
.pill.active{border-color:var(--link); color:var(--link)}
.pill.ok{background:var(--ok-bg); border-color:var(--ok-bd); color:var(--ok-fg)}
.pill.err{background:var(--err-bg); border-color:var(--err-bd); color:var(--err-fg)}
.pill.warn{background:var(--warn-bg); border-color:var(--warn-bd); color:var(--warn-fg)}

/* List */
ul{list-style:none; margin:0; padding:0}
li{
border-top:1px solid var(--border);
padding:16px 8px;
}
li:first-child{border-top:1px solid var(--border)}
li:hover{background:var(--row-hover)}
.row{
display:flex; gap:12px; align-items:center; justify-content:space-between; flex-wrap:wrap;
}
.title{
display:flex; gap:10px; align-items:center; min-width:280px;
}
.folder{font-weight:650}
.ts{color:var(--muted); font-size:.9em}
.badges{display:flex; gap:6px; flex-wrap:wrap}
.chip{
background:var(--chip); color:var(--chipfg);
border:1px solid var(--chip-bd); border-radius:999px;
padding:2px 10px; font-size:.85em;
}
.chip.ok{background:var(--ok-bg);border-color:var(--ok-bd);color:var(--ok-fg)}
.chip.err{background:var(--err-bg);border-color:var(--err-bd);color:var(--err-fg)}
.chip.warn{background:var(--warn-bg);border-color:var(--warn-bd);color:var(--warn-fg)}
.chip.mono{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace}

.counts{color:var(--muted); font-size:.9em; margin-top:4px}
</style>

<h1>Playwright Reports</h1>
<p class="meta" id="updated">Updated …</p>

<div class="toolbar">
<div class="pills" id="statusPills">
<span class="pill active" data-status="">All</span>
<span class="pill ok" data-status="ok">Passed</span>
<span class="pill err" data-status="err">Failed</span>
<span class="pill warn" data-status="warn">Flaky</span>
</div>
<select id="branchFilter">
<option value="">All branches</option>
</select>
<input id="searchBox" type="search" placeholder="Search message / folder / SHA / actor">
</div>

<ul id="list"><li>Loading…</li></ul>

<script>
(async () => {
const feedRes = await fetch('feed.json', { cache: 'no-store' });
if (!feedRes.ok) {
document.getElementById('list').innerHTML = '<li>Failed to load feed.json</li>';
return;
}
const feed = await feedRes.json();

// Sort newest first using updated/date
const items = (feed.items || []).slice().sort((a, b) => {
const ta = Date.parse(a.updated || a.date || 0) || 0;
const tb = Date.parse(b.updated || b.date || 0) || 0;
return tb - ta;
});

// Page "Updated …" stamp
document.getElementById('updated').textContent =
`Updated ${feed.updated || ''}`;

// Branch dropdown
const branches = [...new Set(items.map(i => i.branch).filter(Boolean))].sort();
const branchSel = document.getElementById('branchFilter');
branches.forEach(b => { const o=document.createElement('option'); o.value=b; o.textContent=b; branchSel.appendChild(o); });

const searchBox = document.getElementById('searchBox');
const ul = document.getElementById('list');

// Status pills
let statusFilter = '';
const pills = document.getElementById('statusPills');
pills.addEventListener('click', (e)=>{
const t = e.target.closest('.pill');
if (!t) return;
statusFilter = t.dataset.status || '';
[...pills.children].forEach(el => el.classList.toggle('active', el===t));
render();
});

function statusFromOutcome(outcome){
if (!outcome) return '';
const o = String(outcome).toLowerCase();
if (o === 'success' || o === 'passed' || o === 'pass') return 'ok';
if (o === 'failure' || o === 'failed') return 'err';
if (o === 'cancelled' || o === 'flaky') return 'warn';
return '';
}

function classifyStatus(stats, fallbackOutcome){
if (stats && (stats.failed > 0)) return 'err';
if (stats && (stats.flaky > 0)) return 'warn';
if (stats && (stats.passed > 0) && stats.failed === 0) return 'ok';
return statusFromOutcome(fallbackOutcome);
}

function chip(label, cls=''){ return `<span class="chip ${cls}">${label}</span>`; }

async function fetchStatsFor(item){
// Use reportUrl from the new feed format
const base = item.reportUrl || item.url || '';
if (!base) return null;

// If meta.json already included stats, use them
if (item.stats && (item.stats.total || item.stats.passed || item.stats.failed || item.stats.flaky)) {
return item.stats;
}

try {
const res = await fetch(`${base}data/report.json`, { cache:'no-store' });
if (!res.ok) return null;
const data = await res.json();

// Playwright 1.40+ has stats at data.stats
if (data && data.stats && typeof data.stats === 'object') {
const s = data.stats;
const total = Number(s.total ?? (s.expected + s.unexpected + s.skipped + s.flaky)) || 0;
return {
total,
passed: Number(s.expected ?? s.passed ?? 0),
failed: Number(s.unexpected ?? s.failed ?? 0),
skipped: Number(s.skipped ?? 0),
flaky: Number(s.flaky ?? 0),
durationMs: Number(s.duration ?? s.durationMs ?? 0)
};
}

// Fallback: tally suites/tests if needed
function tallySuite(suite){
const acc = {total:0,passed:0,failed:0,skipped:0,flaky:0,durationMs:0};
if (!suite) return acc;
(suite.suites||[]).forEach(ss => { const t=tallySuite(ss); for (const k in acc) acc[k]+=t[k]; });
(suite.tests||[]).forEach(t => {
acc.total++;
const s = t.outcome || t.status || 'unknown';
if (s==='expected' || s==='passed') acc.passed++;
else if (s==='unexpected' || s==='failed') acc.failed++;
else if (s==='flaky') acc.flaky++;
else if (s==='skipped') acc.skipped++;
acc.durationMs += Number(t.duration || t.durationMs || 0);
});
return acc;
}
if (data && (Array.isArray(data.suites) || data.suites)) return tallySuite(data);
return null;
} catch { return null; }
}

function safeUTC(ts){
const d = ts ? new Date(ts) : null;
return (d && !isNaN(d)) ? d.toUTCString() : '—';
}

async function render(){
const branch = branchSel.value;
const q = searchBox.value.trim().toLowerCase();

const rows = await Promise.all(items.map(async (d) => {
const stats = await fetchStatsFor(d);
const statusCls = classifyStatus(stats, d.outcome);

// Filtering
if (branch && d.branch !== branch) return null;
if (statusFilter && statusCls !== statusFilter) return null;

// Title + links from new feed fields
const titleText = d.title || (d.repo ? `${d.repo}@${d.shortSha||''}` : 'Report');
const reportHref = d.reportUrl || d.url || '#';

// Commit link (build if not provided)
const commitHref = d.commitUrl
|| (d.repo && d.commitSha ? `https://github.com/${d.repo}/commit/${d.commitSha}` : '');
const hashText = d.shortSha ? `#${d.shortSha}` : '';

const ts = d.updated || d.date || '';
const when = safeUTC(ts);

// Search text
const text = [
d.title, d.subtitle, d.branch, d.actor, d.repo, d.runId,
d.commitSha, d.shortSha, d.outcome
].filter(Boolean).join(' ').toLowerCase();
if (q && !text.includes(q)) return null;

const counts = stats
? `• ${stats.passed ?? 0} passed, ${stats.failed ?? 0} failed, ${stats.flaky ?? 0} flaky, ${stats.skipped ?? 0} skipped`
: '';

const badges = [
d.branch && chip(d.branch),
d.actor && chip('@'+d.actor),
d.repo && chip(d.repo),
d.runId && chip(`run ${d.runId}`),
statusCls === 'ok' && chip('Passed','ok'),
statusCls === 'err' && chip('Failed','err'),
statusCls === 'warn' && chip('Flaky','warn')
].filter(Boolean).join(' ');

return `
<li>
<div class="row">
<div class="title">
<a class="folder" href="${reportHref}" rel="noopener noreferrer">${titleText}</a>
${commitHref && hashText ? `<a class="chip mono" style="padding:1px 6px" href="${commitHref}" rel="noopener noreferrer">${hashText}</a>` : ''}
<span class="ts">${when}</span>
</div>
<div class="badges">${badges}</div>
</div>
<div class="counts">
${d.subtitle ? `<a href="${reportHref}" rel="noopener noreferrer">${d.subtitle}</a>` : `<a href="${reportHref}" rel="noopener noreferrer">Open report</a>`}
${counts ? counts : ''}
</div>
</li>`;
}));

document.getElementById('list').innerHTML =
rows.filter(Boolean).join('') || '<li class="ts">No matching reports.</li>';
}

branchSel.addEventListener('change', render);
searchBox.addEventListener('input', render);
render();
})();
</script>
Loading