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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ uv run --extra=dev tox -e e2e

Note: E2E tests require the frontend to be built first. Run `npm run build` or `npm run start` in `server/frontend/` before running E2E tests.

E2E test data lives in `tests/e2e/fixtures/fixtures.json` and is generated by `tests/e2e/scripts/generate_e2e_fixtures.py`. Edit `BUCKETS_CONFIG` in that script to add or modify test buckets and their reports, then regenerate the fixture file:

```
uv run --extra=server python tests/e2e/scripts/generate_e2e_fixtures.py
```

To run all python tests excluding E2E tests:

Expand Down
16 changes: 14 additions & 2 deletions server/frontend/src/components/Buckets/ReportPreviewRow.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<template>
<tr @click="report.view_url">
<tr @click="report.view_url" :class="{ warning: isNewAfterUntriage }">
<td class="wrap-normal">
{{ formatShorterDate(report.reported_at) }}<br />
<span v-if="isNewAfterUntriage" class="label label-warning">new</span
><br />
{{ formatShorterDate(report.reported_at) }}
(<a :href="report.view_url">Full details</a>)
</td>
<td class="url-col">
Expand Down Expand Up @@ -96,6 +98,16 @@ export default {
type: Object,
required: true,
},
triagedAt: {
type: String,
default: null,
},
},
computed: {
isNewAfterUntriage() {
if (!this.triagedAt || !this.report.reported_at) return false;
return new Date(this.report.reported_at) > new Date(this.triagedAt);
},
},
methods: {
formatShorterDate: shorterDate,
Expand Down
23 changes: 23 additions & 0 deletions server/frontend/src/components/Buckets/View.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@
</div>
</td>
<td v-else>
<div
v-if="lastTriagedAt"
class="alert alert-warning last-triaged"
data-testid="untriage-warning"
>
<i class="bi bi-exclamation-triangle-fill"></i>
This bucket was last triaged on
{{ formatDate(lastTriagedAt) }} but was automatically untriaged
due to a spike in activity. Reports causing the spike are
highlighted below.
</div>
No bug associated.
<span v-if="triageStatus" data-testid="triage-status-display"
>Marked triaged as:
Expand Down Expand Up @@ -208,6 +219,7 @@
v-else
:key="report.id"
:report="report"
:triaged-at="lastTriagedAt"
/>
</tbody>
</table>
Expand Down Expand Up @@ -313,6 +325,13 @@ export default {
};
},
computed: {
lastTriagedAt() {
if (!this.triageStatus && this.triagedAt) {
return this.triagedAt;
}

return null;
},
prettySignature() {
return jsonPretty(this.bucket.signature);
},
Expand Down Expand Up @@ -586,4 +605,8 @@ button[aria-expanded="false"] .bi-eye-slash-fill {
padding: 0;
margin: 0.25em 0 0;
}

.last-triaged {
max-width: 500px;
}
</style>
87 changes: 87 additions & 0 deletions tests/e2e/fixtures/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,22 @@
"cluster": 99999
}
},
{
"model": "reportmanager.bucket",
"pk": 888888,
"fields": {
"bug": null,
"color": null,
"description": "www.example-spike.com [Cluster 88888]",
"domain": "www.example-spike.com",
"triage_status": null,
"triaged_at": "2026-04-10T18:05:00+00:00",
"priority": 0,
"signature": "{\"symptoms\": [{\"type\": \"url\", \"part\": \"hostname\", \"value\": \"www.example-spike.com\"}]}",
"reassign_in_progress": false,
"cluster": 88888
}
},
{
"model": "reportmanager.reportentry",
"pk": 1000,
Expand Down Expand Up @@ -672,6 +688,69 @@
"cluster": 99999
}
},
{
"model": "reportmanager.reportentry",
"pk": 1021,
"fields": {
"app": 1,
"breakage_category": 1,
"bucket": 888888,
"comments": "Old report before triage",
"comments_translated": "Old report before triage",
"comments_original_language": "en",
"comments_preprocessed": "old report before triage",
"details": {},
"domain": "www.example-spike.com",
"ml_valid_probability": 0.9,
"os": 1,
"reported_at": "2026-04-08T18:05:00+00:00",
"url": "https://www.example-spike.com/",
"uuid": "00000000-0000-0000-0000-000000001021",
"cluster": 88888
}
},
{
"model": "reportmanager.reportentry",
"pk": 1022,
"fields": {
"app": 2,
"breakage_category": 1,
"bucket": 888888,
"comments": "New report after spike untriage",
"comments_translated": "New report after spike untriage",
"comments_original_language": "en",
"comments_preprocessed": "new report after spike untriage",
"details": {},
"domain": "www.example-spike.com",
"ml_valid_probability": 0.88,
"os": 2,
"reported_at": "2026-04-12T18:05:00+00:00",
"url": "https://www.example-spike.com/",
"uuid": "00000000-0000-0000-0000-000000001022",
"cluster": 88888
}
},
{
"model": "reportmanager.reportentry",
"pk": 1023,
"fields": {
"app": 3,
"breakage_category": 2,
"bucket": 888888,
"comments": "Another new report after spike untriage",
"comments_translated": "Another new report after spike untriage",
"comments_original_language": "en",
"comments_preprocessed": "another new report after spike untriage",
"details": {},
"domain": "www.example-spike.com",
"ml_valid_probability": 0.85,
"os": 3,
"reported_at": "2026-04-14T18:05:00+00:00",
"url": "https://www.example-spike.com/",
"uuid": "00000000-0000-0000-0000-000000001023",
"cluster": 88888
}
},
{
"model": "reportmanager.cluster",
"pk": 70854,
Expand Down Expand Up @@ -711,5 +790,13 @@
"domain": "www.example-triaged.com",
"centroid": 1019
}
},
{
"model": "reportmanager.cluster",
"pk": 88888,
"fields": {
"domain": "www.example-spike.com",
"centroid": 1021
}
}
]
9 changes: 8 additions & 1 deletion tests/e2e/fixtures/reports_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.management import call_command

from reportmanager.models import Bucket
from reportmanager.models import Bucket, ReportEntry
from reportmanager.models import User as ReportManagerUser

from .defaults import TEST_USER_EMAIL, TEST_USER_PASSWORD
Expand All @@ -28,6 +28,9 @@ def e2e_data(db):
)
user.user_permissions.add(write_perm)

spike_bucket = Bucket.objects.get(pk=888888)
spike_triaged_at = spike_bucket.triaged_at

return {
"bucket_count": Bucket.objects.count(),
"triaged_bucket_id": Bucket.objects.filter(triage_status__isnull=False)
Expand All @@ -39,4 +42,8 @@ def e2e_data(db):
.first()
.id,
"triaged_count": Bucket.objects.filter(triage_status__isnull=False).count(),
"spike_bucket_id": spike_bucket.id,
"spike_new_report_count": ReportEntry.objects.filter(
bucket=spike_bucket, reported_at__gt=spike_triaged_at
).count(),
}
4 changes: 4 additions & 0 deletions tests/e2e/page_objects/bucket_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def __init__(self, page, live_server, bucket_id):
self.triage_status_display = self.page.get_by_test_id("triage-status-display")
self.triage_status_label = self.page.get_by_test_id("triage-status-label")
self.unmark_option = self.page.get_by_test_id("triage-unmark")
self.untriage_warning = self.page.get_by_test_id("untriage-warning")

def wait_for_loaded(self):
"""Wait for the bucket view's triage trigger to be visible."""
Expand All @@ -39,3 +40,6 @@ def expect_triage_label(self, text):

def expect_triage_trigger(self, text):
expect(self.triage_trigger).to_have_text(text)

def highlighted_report_rows(self):
return self.page.locator("tr.warning")
49 changes: 45 additions & 4 deletions tests/e2e/scripts/generate_e2e_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,45 @@
},
],
},
{
"id": 888888,
"domain": "www.example-spike.com",
"description": "www.example-spike.com [Cluster 88888]",
"cluster_id": 88888,
"priority": 0,
"triage_status": None,
"triaged_at": ANCHOR_DATE - timedelta(days=5),
"signature": '{"symptoms": [{"type": "url", "part": "hostname", "value": "www.example-spike.com"}]}',
"comments": [
{
# reported 7 days before ANCHOR_DATE = 2 days before triaged_at -> not highlighted
"reported_at": ANCHOR_DATE - timedelta(days=7),
"comments": "Old report before triage",
"comments_translated": "Old report before triage",
"comments_original_language": "en",
"breakage_category": "load",
"ml_valid_probability": 0.9,
},
{
# reported 3 days before ANCHOR_DATE = 2 days after triaged_at -> highlighted
"reported_at": ANCHOR_DATE - timedelta(days=3),
"comments": "New report after spike untriage",
"comments_translated": "New report after spike untriage",
"comments_original_language": "en",
"breakage_category": "load",
"ml_valid_probability": 0.88,
},
{
# reported 1 day before ANCHOR_DATE = 4 days after triaged_at -> highlighted
"reported_at": ANCHOR_DATE - timedelta(days=1),
"comments": "Another new report after spike untriage",
"comments_translated": "Another new report after spike untriage",
"comments_original_language": "en",
"breakage_category": "media",
"ml_valid_probability": 0.85,
},
],
},
]

# Supporting data
Expand Down Expand Up @@ -375,7 +414,11 @@ def generate_fixtures():
comments_translated.lower() if comments_translated else ""
)

days_ago = date_offsets[i % len(date_offsets)]
if "reported_at" in comment_data:
reported_at = comment_data["reported_at"]
else:
days_ago = date_offsets[i % len(date_offsets)]
reported_at = ANCHOR_DATE - timedelta(days=days_ago)

fixture.append(
{
Expand All @@ -396,9 +439,7 @@ def generate_fixtures():
"domain": bucket_config["domain"],
"ml_valid_probability": comment_data["ml_valid_probability"],
"os": (i % len(OS_DATA)) + 1, # Rotate through all OS
"reported_at": (
ANCHOR_DATE - timedelta(days=days_ago)
).isoformat(),
"reported_at": reported_at.isoformat(),
"url": f"https://{bucket_config['domain']}/",
"uuid": f"00000000-0000-0000-0000-{report_id:012d}",
"cluster": bucket_config["cluster_id"],
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/test_bucket_view.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from playwright.sync_api import expect

from .page_objects.bucket_view import BucketViewPage

Expand Down Expand Up @@ -50,3 +51,16 @@ def test_change_and_unmark_triage_status(bucket_view_page, e2e_data):
bucket_view.unmark_triage()
bucket_view.expect_triage_trigger("Mark triaged")
assert bucket_view.triage_status_display.count() == 0


@pytest.mark.ui
@pytest.mark.django_db(transaction=True)
def test_spike_untriage_warning_shown(bucket_view_page, e2e_data):
bucket_view = bucket_view_page(e2e_data["spike_bucket_id"])

# Warning banner should be visible since triage_status is None but triaged_at is set
expect(bucket_view.untriage_warning).to_be_visible()
expect(bucket_view.untriage_warning).to_contain_text("spike in activity")
expect(bucket_view.highlighted_report_rows()).to_have_count(
e2e_data["spike_new_report_count"]
)
Loading