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: 2 additions & 3 deletions packages/app/cypress/e2e/navigation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@ describe('First-load navigation', () => {
win.localStorage.removeItem('inferencex-starred');
win.localStorage.removeItem('inferencex-star-modal-dismissed');
win.localStorage.removeItem('inferencex-dsv4-modal-dismissed');
win.localStorage.removeItem('inferencex-dsv4-banner-dismissed');
},
});

// dsv4 launch modal takes precedence over the GitHub star modal on first
// load — only one modal shows at a time. Either is fine for this test, we
// just need *a* first-load modal up to verify it doesn't block navigation.
// Banner (inline) and overlay modal coexist in independent slots.
cy.get('[data-testid="dsv4-launch-modal"]').should('be.visible');
cy.get('body').should('not.have.attr', 'data-scroll-locked');
});
Expand Down
242 changes: 242 additions & 0 deletions packages/app/cypress/e2e/nudge-system.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/**
* E2E tests for the unified NudgeEngine.
*
* Covers: landing modals (priority ordering, dismissal persistence),
* landing banner, dashboard toasts, evaluation toast, and the
* permanent-suppress ("starred") cross-nudge mechanism.
*/

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function clearAllNudgeStorage(win: Cypress.AUTWindow) {
const keys = [
'inferencex-starred',
'inferencex-star-modal-dismissed',
'inferencex-dsv4-modal-dismissed',
'inferencex-dsv4-banner-dismissed',
'inferencex-reproducibility-nudge-shown',
'inferencex-star-nudge-shown',
'inferencex-export-nudge-shown',
'inferencex-gradient-nudge-shown',
'inferencex-eval-samples-nudge-dismissed',
];
for (const key of keys) {
win.localStorage.removeItem(key);
win.sessionStorage.removeItem(key);
}
}

// ---------------------------------------------------------------------------
// Landing — modal priority & dismissal
// ---------------------------------------------------------------------------

describe('Landing nudges — modals', () => {
it('shows dsv4 modal and banner simultaneously on fresh first load', () => {
cy.visit('/', {
onBeforeLoad: clearAllNudgeStorage,
});
// Banner (inline) and modal (overlay) occupy independent slots
cy.get('[data-testid="launch-banner"]').should('be.visible');
cy.get('[data-testid="dsv4-launch-modal"]').should('be.visible');
// Only one overlay at a time — star modal should not appear
cy.get('[data-testid="github-star-modal"]').should('not.exist');
});

it('dismissing dsv4 modal persists — not shown on reload', () => {
cy.visit('/', {
onBeforeLoad: clearAllNudgeStorage,
});
cy.get('[data-testid="dsv4-launch-modal"]').should('be.visible');
cy.get('[data-testid="dsv4-launch-modal-dismiss"]').click();
cy.get('[data-testid="dsv4-launch-modal"]').should('not.exist');

cy.reload();
cy.get('[data-testid="dsv4-launch-modal"]').should('not.exist');
});

it('shows star modal when dsv4 modal was previously dismissed', () => {
cy.visit('/', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
win.localStorage.setItem('inferencex-dsv4-modal-dismissed', '1');
},
});
cy.get('[data-testid="dsv4-launch-modal"]').should('not.exist');
cy.get('[data-testid="github-star-modal"]').should('be.visible');
});

it('star modal dismiss uses timed strategy — re-shows after expiry', () => {
cy.visit('/', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
win.localStorage.setItem('inferencex-dsv4-modal-dismissed', '1');
},
});
cy.get('[data-testid="github-star-modal"]').should('be.visible');
cy.get('[data-testid="github-star-modal-dismiss"]').click();
cy.get('[data-testid="github-star-modal"]').should('not.exist');

cy.window().then((win) => {
const value = win.localStorage.getItem('inferencex-star-modal-dismissed');
expect(value).to.not.equal(null);
expect(Number(value)).to.be.greaterThan(0);
});
});

it('starring permanently suppresses both star modal and star nudge', () => {
cy.visit('/', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
win.localStorage.setItem('inferencex-dsv4-modal-dismissed', '1');
},
});
cy.get('[data-testid="github-star-modal"]').should('be.visible');
cy.get('[data-testid="github-star-modal-action"]').click();
cy.get('[data-testid="github-star-modal"]').should('not.exist');

cy.window().then((win) => {
expect(win.localStorage.getItem('inferencex-starred')).to.eq('1');
});
});
});

// ---------------------------------------------------------------------------
// Landing — banner
// ---------------------------------------------------------------------------

describe('Landing nudges — banner', () => {
it('shows launch banner on landing page', () => {
cy.visit('/', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
// Dismiss modals so the banner (highest priority at 60) is the active nudge.
// Actually the banner has priority 60 > dsv4 modal 50, so it should show first.
// But the engine only shows one nudge at a time; the banner wins because of priority.
},
});
// The banner has the highest priority (60), so it should appear.
// However, NudgeEngine only shows one nudge at a time.
// With immediate triggers and priority 60 > 50 > 40, the banner wins.
cy.get('[data-testid="launch-banner"]').should('be.visible');
});

it('banner renders within container constraints (not full-width)', () => {
cy.visit('/', {
onBeforeLoad: clearAllNudgeStorage,
});
cy.get('[data-testid="launch-banner"]').should('be.visible');
// The banner's parent section has the container class for width constraints
cy.get('[data-testid="launch-banner"]').parent('section.container').should('exist');
});

it('dismissing the banner persists across reloads', () => {
cy.visit('/', {
onBeforeLoad: clearAllNudgeStorage,
});
cy.get('[data-testid="launch-banner"]').should('be.visible');
cy.get('[data-testid="launch-banner-dismiss"]').click();
cy.get('[data-testid="launch-banner"]').should('not.exist');

cy.reload();
cy.get('[data-testid="launch-banner"]').should('not.exist');
});
});

// ---------------------------------------------------------------------------
// Dashboard — reproducibility toast
// ---------------------------------------------------------------------------

describe('Dashboard nudges — reproducibility toast', () => {
it('shows reproducibility nudge after 1.5s delay on dashboard', () => {
cy.visit('/inference', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
},
});
// Should not be visible immediately
cy.get('[data-testid="reproducibility-nudge"]').should('not.exist');
// After the timer fires (~1.5s + buffer)
cy.get('[data-testid="reproducibility-nudge"]', { timeout: 4000 }).should('be.visible');
});

it('reproducibility nudge is session-only — gone after reload', () => {
cy.visit('/inference', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
},
});
cy.get('[data-testid="reproducibility-nudge"]', { timeout: 4000 }).should('be.visible');

// Session storage should be set
cy.window().then((win) => {
expect(win.sessionStorage.getItem('inferencex-reproducibility-nudge-shown')).to.not.equal(
null,
);
});
});
});

// ---------------------------------------------------------------------------
// Evaluation — eval-samples toast
// ---------------------------------------------------------------------------

describe('Evaluation nudges — eval-samples toast', () => {
it('shows eval-samples nudge after delay on evaluation page', () => {
cy.visit('/evaluation', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
},
});
cy.get('[data-testid="eval-samples-nudge"]', { timeout: 4000 }).should('be.visible');
});

it('eval-samples nudge uses timed dismissal (localStorage timestamp)', () => {
cy.visit('/evaluation', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
},
});
cy.get('[data-testid="eval-samples-nudge"]', { timeout: 4000 }).should('be.visible');

// The engine marks it dismissed on show — verify a timestamp is stored
cy.window().then((win) => {
const value = win.localStorage.getItem('inferencex-eval-samples-nudge-dismissed');
expect(value).to.not.equal(null);
expect(Number(value)).to.be.greaterThan(0);
});
});
});

// ---------------------------------------------------------------------------
// Cross-scope isolation
// ---------------------------------------------------------------------------

describe('Nudge scope isolation', () => {
it('landing nudges do not appear on dashboard', () => {
cy.visit('/inference', {
onBeforeLoad: clearAllNudgeStorage,
});
cy.get('[data-testid="dsv4-launch-modal"]').should('not.exist');
cy.get('[data-testid="github-star-modal"]').should('not.exist');
cy.get('[data-testid="launch-banner"]').should('not.exist');
});

it('dashboard nudges do not appear on landing page', () => {
cy.visit('/', {
onBeforeLoad(win) {
clearAllNudgeStorage(win);
// Dismiss all landing nudges so nothing blocks visibility checks
win.localStorage.setItem('inferencex-dsv4-modal-dismissed', '1');
win.localStorage.setItem('inferencex-dsv4-banner-dismissed', '1');
win.localStorage.setItem('inferencex-starred', '1');
},
});
// Wait a bit for any timer-based nudges
cy.wait(2000);
cy.get('[data-testid="reproducibility-nudge"]').should('not.exist');
cy.get('[data-testid="star-nudge"]').should('not.exist');
cy.get('[data-testid="export-nudge"]').should('not.exist');
});
});
4 changes: 2 additions & 2 deletions packages/app/src/app/(dashboard)/evaluation/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from 'next';

import { EvalSamplesNudge } from '@/components/eval-samples-nudge';
import { EvaluationProvider } from '@/components/evaluation/EvaluationContext';
import EvaluationChartDisplay from '@/components/evaluation/ui/ChartDisplay';
import { NudgeEngine } from '@/components/nudge-engine';
import { tabMetadata } from '@/lib/tab-meta';

export const metadata: Metadata = tabMetadata('evaluation');
Expand All @@ -11,7 +11,7 @@ export default function EvaluationPage() {
return (
<EvaluationProvider>
<EvaluationChartDisplay />
<EvalSamplesNudge />
<NudgeEngine scope="evaluation" />
</EvaluationProvider>
);
}
10 changes: 2 additions & 8 deletions packages/app/src/components/dashboard-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
'use client';

import { ExportNudge } from '@/components/export-nudge';
import { GlobalFilterProvider } from '@/components/GlobalFilterContext';
import { GradientLabelNudge } from '@/components/gradient-label-nudge';
import { ReproducibilityNudge } from '@/components/reproducibility-nudge';
import { StarNudge } from '@/components/star-nudge';
import { NudgeEngine } from '@/components/nudge-engine';
import { TabNav } from '@/components/tab-nav';
import { UnofficialRunProvider } from '@/components/unofficial-run-provider';

export function DashboardShell({ children }: { children: React.ReactNode }) {
return (
<>
<ReproducibilityNudge />
<StarNudge />
<ExportNudge />
<GradientLabelNudge />
<NudgeEngine scope="dashboard" />
<UnofficialRunProvider>
<main className="relative">
<div className="container mx-auto px-4 lg:px-8 flex flex-col gap-4">
Expand Down
92 changes: 0 additions & 92 deletions packages/app/src/components/dsv4-launch-modal.tsx

This file was deleted.

Loading