diff --git a/packages/app/src/specs/SpecNameDisplay.cy.tsx b/packages/app/src/specs/SpecNameDisplay.cy.tsx
new file mode 100644
index 000000000000..7df965a22752
--- /dev/null
+++ b/packages/app/src/specs/SpecNameDisplay.cy.tsx
@@ -0,0 +1,14 @@
+import SpecNameDisplay from './SpecNameDisplay.vue'
+
+describe('', () => {
+ it('should display spec name information', () => {
+ cy.mount()
+
+ cy.findByText('myFileName').should('be.visible')
+ cy.findByText('.cy.tsx').should('be.visible')
+
+ cy.findByTestId('spec-filename').should('have.attr', 'title', 'myFileName.cy.tsx')
+
+ cy.percySnapshot()
+ })
+})
diff --git a/packages/app/src/specs/SpecNameDisplay.vue b/packages/app/src/specs/SpecNameDisplay.vue
new file mode 100644
index 000000000000..200d9f29dee5
--- /dev/null
+++ b/packages/app/src/specs/SpecNameDisplay.vue
@@ -0,0 +1,18 @@
+
+
+ {{ specFileName }}{{ specFileExtension }}
+
+
+
+
diff --git a/packages/app/src/specs/SpecRunSummary.cy.tsx b/packages/app/src/specs/SpecRunSummary.cy.tsx
index e711be4dd4a6..a198d425d3e9 100644
--- a/packages/app/src/specs/SpecRunSummary.cy.tsx
+++ b/packages/app/src/specs/SpecRunSummary.cy.tsx
@@ -8,7 +8,7 @@ function validateTopBorder (color: string): void {
}
function validateFilename (expected: string): void {
- cy.findByTestId('spec-run-filename').should('have.text', expected)
+ cy.findByTestId('spec-filename').should('have.text', expected)
}
function validateTimeAgo (expected: string): void {
diff --git a/packages/app/src/specs/SpecRunSummary.vue b/packages/app/src/specs/SpecRunSummary.vue
index 66186a71a843..0f1f6573560f 100644
--- a/packages/app/src/specs/SpecRunSummary.vue
+++ b/packages/app/src/specs/SpecRunSummary.vue
@@ -5,12 +5,10 @@
:class="highlightColor"
data-cy="spec-run-summary"
>
-
', () => {
+ it('should render expected content', () => {
+ cy.mount(
)
+
+ cy.findByTestId(flakyBadgeTestId)
+ .should('have.text', defaultMessages.specPage.flaky.badgeLabel)
+ .and('be.visible')
+
+ cy.percySnapshot()
+ })
+})
diff --git a/packages/app/src/specs/flaky-badge/FlakyBadge.vue b/packages/app/src/specs/flaky-badge/FlakyBadge.vue
new file mode 100644
index 000000000000..cc8447999a66
--- /dev/null
+++ b/packages/app/src/specs/flaky-badge/FlakyBadge.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx b/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx
new file mode 100644
index 000000000000..e8cc56ae77e2
--- /dev/null
+++ b/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx
@@ -0,0 +1,155 @@
+import FlakySpecSummary from './FlakySpecSummary.vue'
+
+describe('
', () => {
+ it('low severity', () => {
+ cy.mount(
+
,
+ )
+
+ cy.percySnapshot()
+ })
+
+ it('medium severity', () => {
+ cy.mount(
+
,
+ )
+
+ cy.percySnapshot()
+ })
+
+ it('high severity', () => {
+ cy.mount(
+
,
+ )
+
+ cy.percySnapshot()
+ })
+
+ it('fallback state', () => {
+ // Ensure component handles malformed/incomplete data without blowing up
+ cy.mount(
+
,
+ )
+
+ cy.percySnapshot()
+ })
+
+ describe('flaky rate percentages', () => {
+ it('should round up to next integer if less than 99%', () => {
+ cy.mount(
+
,
+ )
+
+ cy.findByTestId('flaky-rate').should('have.text', '89% flaky rate')
+ })
+
+ it('should round down if between 99 and 100%', () => {
+ cy.mount(
+
,
+ )
+
+ cy.findByTestId('flaky-rate').should('have.text', '99% flaky rate')
+ })
+ })
+
+ describe('pluralization', () => {
+ it('should handle zero flaky runs and zero runs since last flake', () => {
+ cy.mount(
+
,
+ )
+
+ cy.findByTestId('flaky-runs').should('have.text', '0 flaky runs / 1000 total')
+ cy.findByTestId('last-flaky').should('have.text', 'Last flaky 0 runs ago')
+ })
+
+ it('should handle 1 flaky run and 1 run since last flake', () => {
+ cy.mount(
+
,
+ )
+
+ cy.findByTestId('flaky-runs').should('have.text', '1 flaky run / 1000 total')
+ cy.findByTestId('last-flaky').should('have.text', 'Last flaky 1 run ago')
+ })
+
+ it('should handle multiple flaky runs and multiple runs since last flake', () => {
+ cy.mount(
+
,
+ )
+
+ cy.findByTestId('flaky-runs').should('have.text', '2 flaky runs / 1000 total')
+ cy.findByTestId('last-flaky').should('have.text', 'Last flaky 2 runs ago')
+ })
+ })
+})
diff --git a/packages/app/src/specs/flaky-badge/FlakySpecSummary.vue b/packages/app/src/specs/flaky-badge/FlakySpecSummary.vue
new file mode 100644
index 000000000000..f11c020de491
--- /dev/null
+++ b/packages/app/src/specs/flaky-badge/FlakySpecSummary.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+ {{ severity?.label }}
+ {{ t('specPage.flaky.flakyRate', [flakyRate]) }}
+
+
+
+ {{ t('specPage.flaky.flakyRuns', { count: totalFlakyRuns, flakyRuns: totalFlakyRuns, totalRuns }) }}
+ {{ t('specPage.flaky.lastFlaky', { count: runsSinceLastFlake, runsSinceLastFlake }) }}
+
+
+
+
+
diff --git a/packages/frontend-shared/src/assets/icons/rate-high_x16.svg b/packages/frontend-shared/src/assets/icons/rate-high_x16.svg
new file mode 100644
index 000000000000..5475db1490ed
--- /dev/null
+++ b/packages/frontend-shared/src/assets/icons/rate-high_x16.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/frontend-shared/src/assets/icons/rate-low_x16.svg b/packages/frontend-shared/src/assets/icons/rate-low_x16.svg
new file mode 100644
index 000000000000..a287db1e093e
--- /dev/null
+++ b/packages/frontend-shared/src/assets/icons/rate-low_x16.svg
@@ -0,0 +1,9 @@
+
diff --git a/packages/frontend-shared/src/assets/icons/rate-medium_x16.svg b/packages/frontend-shared/src/assets/icons/rate-medium_x16.svg
new file mode 100644
index 000000000000..8ecd3a89e60d
--- /dev/null
+++ b/packages/frontend-shared/src/assets/icons/rate-medium_x16.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json
index faab7392adf1..d20fa95a337c 100644
--- a/packages/frontend-shared/src/locales/en-US.json
+++ b/packages/frontend-shared/src/locales/en-US.json
@@ -151,6 +151,15 @@
"linkText": "average spec durations"
}
},
+ "flaky": {
+ "badgeLabel": "Flaky",
+ "severityLow": "Low",
+ "severityMedium": "Medium",
+ "severityHigh": "High",
+ "flakyRate": "{0}% flaky rate",
+ "flakyRuns": "{flakyRuns} flaky runs / {totalRuns} total | {flakyRuns} flaky run / {totalRuns} total | {flakyRuns} flaky runs / {totalRuns} total",
+ "lastFlaky": "Last flaky {runsSinceLastFlake} runs ago | Last flaky {runsSinceLastFlake} run ago | Last flaky {runsSinceLastFlake} runs ago"
+ },
"connectProjectButton": "Connect your project",
"dashboardLoginButton": "Log in to the Dashboard",
"reconnectProjectButton": "Reconnect your project",