From ea6e9c6059ef8e0bf70dd4a0326594536d68b75f Mon Sep 17 00:00:00 2001 From: verte Date: Fri, 30 Jan 2026 08:14:58 -0500 Subject: [PATCH 1/5] chore(tests): Fix unintended mocking --- components/ui/Button.tsx | 4 +- components/ui/Header.tsx | 8 ++-- components/ui/PauseMenu.tsx | 68 ++++++++++++++++-------------- components/ui/PrivacyBadge.tsx | 6 +-- design-system.ts | 61 +++++++++++++++++++++++++++ style.css | 36 ++++++++++++++++ tests/e2e/fixtures.ts | 7 ++- tests/e2e/setup.ts | 7 ++- tests/e2e/utils/error-helpers.ts | 1 + tests/e2e/utils/product-helpers.ts | 1 + tests/e2e/utils/wait-helpers.ts | 1 + views/BackToAnOldFlame.tsx | 21 +++------ views/Celebration.tsx | 19 +++------ views/EarlyReturnFromSleep.tsx | 22 +++------- views/IDontNeedIt.tsx | 21 +++------ views/ProductView.tsx | 25 +++++------ views/SleepOnIt.tsx | 44 ++++--------------- 17 files changed, 195 insertions(+), 157 deletions(-) diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index bc8940d..69da808 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -1,4 +1,4 @@ -import { commonSpacing, spacing, textSize } from "../../design-system" +import { commonSpacing, spacing, textSize, iconSize } from "../../design-system" type ButtonProps = { children: React.ReactNode @@ -72,7 +72,7 @@ const Button = ({ onClick={handleClick} disabled={disabled}> {icon && ( - icon + icon )} {children} diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index 3d089d9..a848d2d 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -1,6 +1,6 @@ import React from "react" -import { commonSpacing, spacing } from "../../design-system" +import { commonSpacing, spacing, layout, iconSize } from "../../design-system" type HeaderProps = { onBack?: () => void @@ -11,9 +11,7 @@ type HeaderProps = { } const headerStyle: React.CSSProperties = { - display: "grid", - gridTemplateColumns: "1fr 1fr 1fr", - alignItems: "center", + ...layout.gridThreeColumn, marginBottom: commonSpacing.itemMargin } @@ -123,7 +121,7 @@ const Header = ({ {centerIconAlt ) : ( centerIcon diff --git a/components/ui/PauseMenu.tsx b/components/ui/PauseMenu.tsx index c129258..67688ca 100644 --- a/components/ui/PauseMenu.tsx +++ b/components/ui/PauseMenu.tsx @@ -1,6 +1,6 @@ import React from "react" -import { spacing, textSize } from "../../design-system" +import { spacing, textSize, typography, layout } from "../../design-system" export type PauseDuration = "close" | "30s" | "1hour" | "1day" @@ -28,17 +28,14 @@ const menuStyle: React.CSSProperties = { padding: spacing.lg, minWidth: "280px", boxShadow: "0 4px 24px rgba(0, 0, 0, 0.15)", - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, + ...typography.title, fontWeight: "600", margin: 0, - color: "#1a1a1a", - textAlign: "center" + color: "#1a1a1a" } const optionsStyle: React.CSSProperties = { @@ -47,24 +44,17 @@ const optionsStyle: React.CSSProperties = { gap: spacing.sm } -const optionButtonStyle: React.CSSProperties = { +const cancelButtonStyle: React.CSSProperties = { padding: `${spacing.md} ${spacing.lg}`, - backgroundColor: "#f5f5f5", - color: "#1a1a1a", + backgroundColor: "transparent", + color: "#666666", border: "1px solid #e0e0e0", borderRadius: "8px", fontSize: textSize.md, cursor: "pointer", - textAlign: "left", - transition: "all 0.2s ease", - fontFamily: "inherit" -} - -const cancelButtonStyle: React.CSSProperties = { - ...optionButtonStyle, - backgroundColor: "transparent", - color: "#666666", textAlign: "center", + transition: "all 0.2s ease", + fontFamily: "inherit", marginTop: spacing.sm } @@ -82,56 +72,72 @@ const PauseMenu = ({ onSelect, onCancel }: PauseMenuProps) => {

Pause notifications?

{isDebugMode && ( )} diff --git a/components/ui/PrivacyBadge.tsx b/components/ui/PrivacyBadge.tsx index 15b23b8..e00f1fb 100644 --- a/components/ui/PrivacyBadge.tsx +++ b/components/ui/PrivacyBadge.tsx @@ -1,4 +1,4 @@ -import { spacing, textSize } from "../../design-system" +import { spacing, textSize, iconSize } from "../../design-system" const PrivacyBadge = () => { const containerStyle: React.CSSProperties = { @@ -21,8 +21,8 @@ const PrivacyBadge = () => { return (
({ get: () => ["en-US", "en"] }) - // Add chrome object + // Add chrome object ONLY on non-extension pages + // Don't override the real chrome API on extension pages // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any).chrome = { runtime: {} } + if (!location.href.startsWith("chrome-extension://")) { + ;(window as any).chrome = { runtime: {} } + } }) await use(context) diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts index 5f4d1e7..add1256 100644 --- a/tests/e2e/setup.ts +++ b/tests/e2e/setup.ts @@ -47,9 +47,12 @@ setup("Verify extension is loadable", async () => { get: () => ["en-US", "en"] }) - // Add chrome object + // Add chrome object ONLY on non-extension pages + // Don't override the real chrome API on extension pages // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any).chrome = { runtime: {} } + if (!location.href.startsWith("chrome-extension://")) { + ;(window as any).chrome = { runtime: {} } + } }) // In headless mode, we can't navigate to chrome:// URLs diff --git a/tests/e2e/utils/error-helpers.ts b/tests/e2e/utils/error-helpers.ts index 1810c58..537fed5 100644 --- a/tests/e2e/utils/error-helpers.ts +++ b/tests/e2e/utils/error-helpers.ts @@ -94,3 +94,4 @@ export async function waitForTabClosureOrTimeout( } } + diff --git a/tests/e2e/utils/product-helpers.ts b/tests/e2e/utils/product-helpers.ts index c4c1bd9..96aeca7 100644 --- a/tests/e2e/utils/product-helpers.ts +++ b/tests/e2e/utils/product-helpers.ts @@ -54,3 +54,4 @@ export function extractProductIdFromUrl(url: string): string | null { return null } + diff --git a/tests/e2e/utils/wait-helpers.ts b/tests/e2e/utils/wait-helpers.ts index ba059d9..88154e8 100644 --- a/tests/e2e/utils/wait-helpers.ts +++ b/tests/e2e/utils/wait-helpers.ts @@ -57,3 +57,4 @@ export async function waitForCelebrationHidden( }) } + diff --git a/views/BackToAnOldFlame.tsx b/views/BackToAnOldFlame.tsx index 7e84dbc..0aae608 100644 --- a/views/BackToAnOldFlame.tsx +++ b/views/BackToAnOldFlame.tsx @@ -4,7 +4,7 @@ import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg" import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" -import { spacing, textSize } from "../design-system" +import { spacing, typography, layout, iconSize } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" @@ -20,18 +20,11 @@ type BackToAnOldFlameProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const actionsStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const BackToAnOldFlame = ({ @@ -132,7 +125,7 @@ const BackToAnOldFlame = ({ thoughtful } /> @@ -142,10 +135,8 @@ const BackToAnOldFlame = ({

Have you made up your mind?

diff --git a/views/Celebration.tsx b/views/Celebration.tsx index 07d3428..1667199 100644 --- a/views/Celebration.tsx +++ b/views/Celebration.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { spacing, textSize } from "../design-system" +import { typography, iconSize } from "../design-system" type CelebrationProps = { icon: string @@ -16,21 +16,12 @@ type CelebrationProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const subtitleStyle: React.CSSProperties = { - fontSize: textSize.md, - color: "var(--text-color-light)", - textAlign: "center", - margin: 0, - opacity: "0.9", - lineHeight: "1.4" + ...typography.subtitle, + margin: 0 } const Celebration = ({ @@ -62,7 +53,7 @@ const Celebration = ({ {iconAlt} } centerIconAlt={iconAlt} diff --git a/views/EarlyReturnFromSleep.tsx b/views/EarlyReturnFromSleep.tsx index d9ae85e..ed309f0 100644 --- a/views/EarlyReturnFromSleep.tsx +++ b/views/EarlyReturnFromSleep.tsx @@ -4,7 +4,7 @@ import clockIcon from "url:../assets/icons/Icons/Clock.svg" import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" -import { spacing, textSize } from "../design-system" +import { typography, layout, iconSize } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" @@ -20,27 +20,15 @@ type EarlyReturnFromSleepProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const subtitleStyle: React.CSSProperties = { - fontSize: textSize.md, - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.xxl} 0`, - opacity: "0.9", - lineHeight: "1.4" + ...typography.subtitle } const actionsStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const EarlyReturnFromSleep = ({ @@ -143,7 +131,7 @@ const EarlyReturnFromSleep = ({ clock } /> diff --git a/views/IDontNeedIt.tsx b/views/IDontNeedIt.tsx index c341efa..2cdc682 100644 --- a/views/IDontNeedIt.tsx +++ b/views/IDontNeedIt.tsx @@ -6,7 +6,7 @@ import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { commonSpacing, spacing, textSize } from "../design-system" +import { commonSpacing, spacing, typography, layout, iconSize } from "../design-system" import Celebration from "./Celebration" // Feature flag: Set to true to show investment options view @@ -18,26 +18,17 @@ type IDontNeedItProps = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xl, - fontWeight: "bold", - color: "var(--text-color-light)", - textAlign: "center", - margin: `0 0 ${spacing.md} 0`, - lineHeight: "1.3" + ...typography.title } const questionStyle: React.CSSProperties = { - fontSize: textSize.lg, - color: "var(--text-color-light)", - textAlign: "center", + ...typography.subtitleLarge, margin: `0 0 ${commonSpacing.sectionMargin} 0`, fontWeight: "500" } const optionsContainerStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md, + ...layout.actionsContainer, width: "100%", marginBottom: commonSpacing.sectionMargin } @@ -83,7 +74,7 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { star } centerIconAlt="star" @@ -119,7 +110,7 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { lightbulb

Did you know? Not saving enough is the #1 regret of diff --git a/views/ProductView.tsx b/views/ProductView.tsx index bd4c307..087c93e 100644 --- a/views/ProductView.tsx +++ b/views/ProductView.tsx @@ -10,8 +10,9 @@ import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PauseMenu, { type PauseDuration } from "../components/ui/PauseMenu" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { spacing, textSize } from "../design-system" +import { spacing, textSize, typography, layout, iconSize } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" +import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" import { storage } from "../storage" import { extractProduct } from "../utils/productExtractor" @@ -35,8 +36,7 @@ const headerStyle: React.CSSProperties = { } const titleStyle: React.CSSProperties = { - fontSize: textSize.xxl, - fontWeight: "bold", + ...typography.titleLarge, margin: "0", display: "flex", alignItems: "center", @@ -45,9 +45,7 @@ const titleStyle: React.CSSProperties = { } const subtitleStyle: React.CSSProperties = { - fontSize: textSize.lg, - color: "var(--text-color-light)", - opacity: "0.8", + ...typography.subtitleLarge, margin: "0", display: "flex", alignItems: "center", @@ -56,20 +54,15 @@ const subtitleStyle: React.CSSProperties = { } const bodyStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const actionsStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: spacing.md + ...layout.actionsContainer } const actionsGroupStyle: React.CSSProperties = { - display: "flex", - gap: spacing.md + ...layout.actionsGroup } const ProductView = ({ @@ -122,6 +115,7 @@ const ProductView = ({ onShowINeedIt() } + const handleCloseClick = () => { setShowPauseMenu(true) } @@ -238,7 +232,7 @@ const ProductView = ({ lightbulb Quick thought before you buy

@@ -252,6 +246,7 @@ const ProductView = ({ onClick={handleIDontNeedIt}> I don't really need it +
diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index a848d2d..3d1cdb0 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -1,6 +1,6 @@ import React from "react" -import { commonSpacing, spacing, layout, iconSize } from "../../design-system" +import { commonSpacing, iconSize, layout, spacing } from "../../design-system" type HeaderProps = { onBack?: () => void diff --git a/components/ui/PauseMenu.tsx b/components/ui/PauseMenu.tsx index 67688ca..42fd78e 100644 --- a/components/ui/PauseMenu.tsx +++ b/components/ui/PauseMenu.tsx @@ -1,6 +1,6 @@ import React from "react" -import { spacing, textSize, typography, layout } from "../../design-system" +import { layout, spacing, textSize, typography } from "../../design-system" export type PauseDuration = "close" | "30s" | "1hour" | "1day" diff --git a/components/ui/PrivacyBadge.tsx b/components/ui/PrivacyBadge.tsx index e00f1fb..6acebd6 100644 --- a/components/ui/PrivacyBadge.tsx +++ b/components/ui/PrivacyBadge.tsx @@ -1,4 +1,4 @@ -import { spacing, textSize, iconSize } from "../../design-system" +import { iconSize, spacing, textSize } from "../../design-system" const PrivacyBadge = () => { const containerStyle: React.CSSProperties = { diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts index 9ebfbd5..4db60dd 100644 --- a/tests/e2e/fixtures.ts +++ b/tests/e2e/fixtures.ts @@ -74,8 +74,9 @@ export const test = base.extend({ // Add chrome object ONLY on non-extension pages // Don't override the real chrome API on extension pages - // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!location.href.startsWith("chrome-extension://")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).chrome = { runtime: {} } } }) diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts index add1256..de23d1b 100644 --- a/tests/e2e/setup.ts +++ b/tests/e2e/setup.ts @@ -49,8 +49,9 @@ setup("Verify extension is loadable", async () => { // Add chrome object ONLY on non-extension pages // Don't override the real chrome API on extension pages - // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!location.href.startsWith("chrome-extension://")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).chrome = { runtime: {} } } }) diff --git a/tests/e2e/utils/error-helpers.ts b/tests/e2e/utils/error-helpers.ts index 537fed5..7de532b 100644 --- a/tests/e2e/utils/error-helpers.ts +++ b/tests/e2e/utils/error-helpers.ts @@ -93,5 +93,3 @@ export async function waitForTabClosureOrTimeout( return page.isClosed() } } - - diff --git a/tests/e2e/utils/product-helpers.ts b/tests/e2e/utils/product-helpers.ts index 96aeca7..31cf739 100644 --- a/tests/e2e/utils/product-helpers.ts +++ b/tests/e2e/utils/product-helpers.ts @@ -53,5 +53,3 @@ export function extractProductIdFromUrl(url: string): string | null { return null } - - diff --git a/tests/e2e/utils/wait-helpers.ts b/tests/e2e/utils/wait-helpers.ts index 88154e8..b2bade8 100644 --- a/tests/e2e/utils/wait-helpers.ts +++ b/tests/e2e/utils/wait-helpers.ts @@ -56,5 +56,3 @@ export async function waitForCelebrationHidden( timeout: timeout || TEST_CONFIG.TIMEOUTS.CELEBRATION_FADE }) } - - diff --git a/views/BackToAnOldFlame.tsx b/views/BackToAnOldFlame.tsx index 0aae608..a3b944d 100644 --- a/views/BackToAnOldFlame.tsx +++ b/views/BackToAnOldFlame.tsx @@ -4,7 +4,7 @@ import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg" import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" -import { spacing, typography, layout, iconSize } from "../design-system" +import { iconSize, layout, typography } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" diff --git a/views/Celebration.tsx b/views/Celebration.tsx index 1667199..a46e86d 100644 --- a/views/Celebration.tsx +++ b/views/Celebration.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { typography, iconSize } from "../design-system" +import { iconSize, typography } from "../design-system" type CelebrationProps = { icon: string diff --git a/views/EarlyReturnFromSleep.tsx b/views/EarlyReturnFromSleep.tsx index ed309f0..3105e79 100644 --- a/views/EarlyReturnFromSleep.tsx +++ b/views/EarlyReturnFromSleep.tsx @@ -4,7 +4,7 @@ import clockIcon from "url:../assets/icons/Icons/Clock.svg" import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" -import { typography, layout, iconSize } from "../design-system" +import { iconSize, layout, typography } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" diff --git a/views/IDontNeedIt.tsx b/views/IDontNeedIt.tsx index 2cdc682..70e9e84 100644 --- a/views/IDontNeedIt.tsx +++ b/views/IDontNeedIt.tsx @@ -6,7 +6,7 @@ import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { commonSpacing, spacing, typography, layout, iconSize } from "../design-system" +import { commonSpacing, iconSize, layout, typography } from "../design-system" import Celebration from "./Celebration" // Feature flag: Set to true to show investment options view @@ -110,7 +110,11 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { lightbulb

Did you know? Not saving enough is the #1 regret of diff --git a/views/ProductView.tsx b/views/ProductView.tsx index 087c93e..7908710 100644 --- a/views/ProductView.tsx +++ b/views/ProductView.tsx @@ -10,9 +10,14 @@ import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PauseMenu, { type PauseDuration } from "../components/ui/PauseMenu" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { spacing, textSize, typography, layout, iconSize } from "../design-system" +import { + iconSize, + layout, + spacing, + textSize, + typography +} from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" -import { ChromeMessaging } from "../services/ChromeMessaging" import type { Product } from "../storage" import { storage } from "../storage" import { extractProduct } from "../utils/productExtractor" @@ -115,7 +120,6 @@ const ProductView = ({ onShowINeedIt() } - const handleCloseClick = () => { setShowPauseMenu(true) } diff --git a/views/SleepOnIt.tsx b/views/SleepOnIt.tsx index c820e2e..e40da11 100644 --- a/views/SleepOnIt.tsx +++ b/views/SleepOnIt.tsx @@ -5,7 +5,7 @@ import Button from "../components/ui/Button" import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" -import { spacing, typography, iconSize } from "../design-system" +import { iconSize, spacing, typography } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import { ChromeMessaging } from "../services/ChromeMessaging" import { storage } from "../storage" From 562cd4103764a8bb12aa2433b3ecd45e631a2d23 Mon Sep 17 00:00:00 2001 From: verte Date: Fri, 30 Jan 2026 08:35:11 -0500 Subject: [PATCH 3/5] chore(content): add agent debug logging --- .cursor/commands/commit.md | 39 ++++++++ .cursor/commands/create-pr.md | 40 ++++++++ contents/amazon.tsx | 28 ++++++ hooks/useProductPageState.ts | 179 ++++++++++++++++++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 .cursor/commands/commit.md create mode 100644 .cursor/commands/create-pr.md diff --git a/.cursor/commands/commit.md b/.cursor/commands/commit.md new file mode 100644 index 0000000..944bdb8 --- /dev/null +++ b/.cursor/commands/commit.md @@ -0,0 +1,39 @@ +--- +description: AGENT STEPS TO COMMIT CHANGES +--- + +Commit the current changes following Conventional Commits specification. + +## Actions + +1. Check git status to see what files have changed +2. Analyze the changes to determine: + - Type: feat, fix, docs, style, refactor, perf, test, chore, ci, build + - Scope: ui, storage, services, notifications, background, content, popup, hooks, managers, docs, build, workflows + - Subject: concise description of the change +3. Check if changes contain breaking changes (API changes, signature changes, etc.) +4. Generate commit message in format: `(): ` +5. Add `!` after scope if breaking change detected +6. Include body if change needs explanation (ensure each body line is ≤100 characters) +7. Execute: `git add -A` +8. Execute: `git commit -m ""` + +## Rules + +- Use lowercase for type and scope +- Subject should be imperative mood, no period at end +- Maximum 72 characters for subject line +- Maximum 100 characters per line in commit body (wrap long lines with proper indentation) +- If breaking change, add `!` after scope or include `BREAKING CHANGE:` in footer +- Scope is optional but recommended + +## Linting, testing and formatting + +- Before committing execute linting, testing and formatting checks: + - `npm run lint` or `npm run lint:fix` + - `npm run format:check` or `npm run format` + - `npm run test:e2e` + +## CRITICAL + +- DO NOT EXCEED THE CHARACTER LIMIT diff --git a/.cursor/commands/create-pr.md b/.cursor/commands/create-pr.md new file mode 100644 index 0000000..32992e0 --- /dev/null +++ b/.cursor/commands/create-pr.md @@ -0,0 +1,40 @@ +--- +description: CREATE PULL REQUEST +--- + +Analyze current changes and create a GitHub PR with appropriate description and labels. + +## Actions + +1. Check current branch name +2. Get git diff to analyze changes +3. Determine PR type from changes: + - `type:feature` - New functionality + - `type:bugfix` - Bug fixes + - `type:breaking` - Breaking changes + - `type:documentation` - Docs only + - `type:refactor` - Code refactoring + - `type:performance` - Performance improvements + - `type:chore` - Maintenance +4. Determine area labels from changed files: + - `area:ui`, `area:storage`, `area:services`, `area:notifications`, `area:background`, `area:content`, `area:popup`, `area:hooks`, `area:managers`, `area:docs`, `area:build`, `area:workflows` +5. Generate PR description with: + - Summary of changes + - List of modified files + - Breaking changes (if any) + - Testing notes +6. Create PR using GitHub CLI or API: + - Title: Follow conventional commit format + - Body: Generated description + - Labels: Type and area labels + - Base branch: master (or development for ongoing work) +7. Output PR URL + +## Rules + +- PR title should follow conventional commit format +- Include clear description of what changed and why +- List breaking changes prominently if present +- Add appropriate labels for changelog generation +- Set base branch to `master` for releases, `development` for ongoing work +- Ensure CODEOWNERS (@vertefra, @emypeeler) are notified for review diff --git a/contents/amazon.tsx b/contents/amazon.tsx index fa10a02..531e4f1 100644 --- a/contents/amazon.tsx +++ b/contents/amazon.tsx @@ -124,8 +124,36 @@ const App = () => { pluginClosed, currentView }) + // #region agent log + fetch("http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "amazon.tsx:121", + message: "Component render check", + data: { shouldShowOverlay, pluginClosed, currentView, reminderView }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H4" + }) + }).catch(() => {}) + // #endregion if (!shouldShowOverlay) { + // #region agent log + fetch("http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "amazon.tsx:129", + message: "OVERLAY HIDDEN - returning null", + data: { pluginClosed, currentView }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H4" + }) + }).catch(() => {}) + // #endregion return null } diff --git a/hooks/useProductPageState.ts b/hooks/useProductPageState.ts index eca9588..b7f5422 100644 --- a/hooks/useProductPageState.ts +++ b/hooks/useProductPageState.ts @@ -95,21 +95,112 @@ export function useProductPageState({ if (productId) { try { + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:97", + message: "Effect started", + data: { productId, currentProductId }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H3" + }) + } + ).catch(() => {}) + // #endregion // Check 1: Global snooze console.log("[useProductPageState] Checking for global snooze...") const snoozedUntil = await storage.getGlobalSnooze() + // #region agent log + const nowTime = Date.now() + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:101", + message: "Snooze check", + data: { + snoozedUntil, + nowTime, + isActive: snoozedUntil && snoozedUntil > nowTime, + diff: snoozedUntil ? snoozedUntil - nowTime : null + }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H1,H5" + }) + } + ).catch(() => {}) + // #endregion if (snoozedUntil && snoozedUntil > Date.now()) { console.log( "[useProductPageState] Global snooze active until:", new Date(snoozedUntil) ) + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:106", + message: "Snooze ACTIVE - hiding overlay", + data: { snoozedUntil, now: Date.now() }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H1,H5" + }) + } + ).catch(() => {}) + // #endregion setPluginClosedState(true) setCurrentView(null) return } else if (snoozedUntil) { // Snooze has expired - clear it console.log("[useProductPageState] Snooze expired, clearing...") + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:112", + message: "Snooze EXPIRED - clearing", + data: { snoozedUntil, now: Date.now() }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H1" + }) + } + ).catch(() => {}) + // #endregion await storage.clearGlobalSnooze() + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:112-after", + message: "After clearGlobalSnooze", + data: {}, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H1" + }) + } + ).catch(() => {}) + // #endregion } // Check 2: Product has terminal state (I_NEED_THIS) @@ -134,10 +225,44 @@ export function useProductPageState({ // Check 3: User global close const globalClosed = await storage.getGlobalPluginClosed() + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:136", + message: "Global plugin closed check", + data: { globalClosed }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H2" + }) + } + ).catch(() => {}) + // #endregion if (globalClosed) { console.log( "[useProductPageState] Global plugin closed - hiding overlay" ) + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:141", + message: "Global CLOSED - hiding overlay", + data: { globalClosed }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H2" + }) + } + ).catch(() => {}) + // #endregion setPluginClosedState(true) setCurrentView(null) return @@ -147,7 +272,41 @@ export function useProductPageState({ console.log( "[useProductPageState] All checks passed - showing overlay" ) + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:150", + message: "ALL CHECKS PASSED - showing overlay", + data: { willSetPluginClosedTo: false }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H4" + }) + } + ).catch(() => {}) + // #endregion setPluginClosedState(false) + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:150-after", + message: "After setPluginClosedState(false)", + data: {}, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H4" + }) + } + ).catch(() => {}) + // #endregion // Check for pending reminders (early return / old flame views) const reminders = await storage.getReminders() @@ -233,6 +392,26 @@ export function useProductPageState({ console.log( "[useProductPageState] Snooze storage changed, re-checking..." ) + // #region agent log + fetch( + "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: "useProductPageState.ts:232", + message: "Snooze storage CHANGED - listener triggered", + data: { + oldValue: changes["thinktwice_snooze"]?.oldValue, + newValue: changes["thinktwice_snooze"]?.newValue + }, + timestamp: Date.now(), + sessionId: "debug-session", + hypothesisId: "H1" + }) + } + ).catch(() => {}) + // #endregion checkForPendingReminder() } From ea0bd2d3b17346f6ece5c32593aa40aeafc5a9fc Mon Sep 17 00:00:00 2001 From: verte Date: Fri, 30 Jan 2026 10:54:11 -0500 Subject: [PATCH 4/5] test(workflows): Extend test workflows and bug fixes --- contents/amazon.tsx | 48 +--- hooks/useProductPageState.ts | 263 +++-------------- tests/e2e/close-flow.spec.ts | 23 +- tests/e2e/extension.spec.ts | 4 +- tests/e2e/fixtures.ts | 8 +- tests/e2e/idontneedit.spec.ts | 26 +- tests/e2e/ineedit.spec.ts | 22 +- tests/e2e/page-objects/OverlayPage.ts | 11 + tests/e2e/setup.ts | 12 +- tests/e2e/sleeponit.spec.ts | 394 ++++++++++++++++++++++++-- tests/e2e/test-config.ts | 29 +- tests/flows/Idontneedit-flow.md | 9 +- views/BackToAnOldFlame.tsx | 14 + views/EarlyReturnFromSleep.tsx | 14 + views/IDontNeedIt.tsx | 18 +- views/ProductView.tsx | 49 ++-- 16 files changed, 580 insertions(+), 364 deletions(-) diff --git a/contents/amazon.tsx b/contents/amazon.tsx index 531e4f1..742d1ca 100644 --- a/contents/amazon.tsx +++ b/contents/amazon.tsx @@ -74,7 +74,7 @@ const App = () => { useGoogleFonts() const { currentView: reminderView, - currentProduct, + product, reminderId, reminderStartTime, pluginClosed, @@ -91,16 +91,7 @@ const App = () => { setLocalView(VIEW.PRODUCT) } - const handleShowIDontNeedIt = async (product: Product | null) => { - // Update product state to dontNeedIt - if (product) { - try { - await ProductActionManager.dontNeedIt(product) - console.log("[Amazon] Product marked as dontNeedIt") - } catch (error) { - console.error("[Amazon] Failed to execute dontNeedIt:", error) - } - } + const handleShowIDontNeedIt = (product: Product | null) => { setLocalView(VIEW.I_DONT_NEED_IT) } @@ -124,36 +115,8 @@ const App = () => { pluginClosed, currentView }) - // #region agent log - fetch("http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "amazon.tsx:121", - message: "Component render check", - data: { shouldShowOverlay, pluginClosed, currentView, reminderView }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H4" - }) - }).catch(() => {}) - // #endregion if (!shouldShowOverlay) { - // #region agent log - fetch("http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "amazon.tsx:129", - message: "OVERLAY HIDDEN - returning null", - data: { pluginClosed, currentView }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H4" - }) - }).catch(() => {}) - // #endregion return null } @@ -166,7 +129,7 @@ const App = () => { if (currentView === VIEW.EARLY_RETURN) { return ( { if (currentView === VIEW.OLD_FLAME) { return ( { return ( (null) - const [currentProduct, setCurrentProduct] = useState(null) + const [product, setProduct] = useState(null) const [reminderId, setReminderId] = useState(null) const [reminderDuration, setReminderDuration] = useState(null) const [reminderStartTime, setReminderStartTime] = useState( @@ -41,7 +41,7 @@ export function useProductPageState({ ) const [pluginClosed, setPluginClosedState] = useState(true) const [tabIdSession, setTabIdSession] = useState(null) - const [currentProductId, setCurrentProductId] = useState(null) + const [urlChangeCounter, setUrlChangeCounter] = useState(0) useEffect(() => { ChromeMessaging.getTabId().then((tabId) => { @@ -50,160 +50,50 @@ export function useProductPageState({ }, []) useEffect(() => { - const updateProductId = () => { - const url = window.location.href - const productId = getProductId(url) - setCurrentProductId((prev) => { - if (prev !== productId) { - console.log( - "[useProductPageState] ProductId changed:", - prev, - "->", - productId - ) - return productId - } - return prev - }) + const handleUrlChange = () => { + console.log("[useProductPageState] URL changed, triggering refresh") + setUrlChangeCounter((prev) => prev + 1) } - // Set initial productId - updateProductId() + // Set initial + handleUrlChange() - // Listen for popstate (browser back/forward) - window.addEventListener("popstate", updateProductId) - - // Listen for full page navigations (when page is fully loaded) - window.addEventListener("load", updateProductId) + // Listen for URL changes + window.addEventListener("popstate", handleUrlChange) + window.addEventListener("load", handleUrlChange) return () => { - window.removeEventListener("popstate", updateProductId) - window.removeEventListener("load", updateProductId) + window.removeEventListener("popstate", handleUrlChange) + window.removeEventListener("load", handleUrlChange) } - }, [getProductId]) + }, []) useEffect(() => { const checkForPendingReminder = async () => { - const productId = currentProductId || getProductId(window.location.href) - console.log( - "[useProductPageState] Using productId:", - productId, - "(from state:", - currentProductId, - ")" - ) + const productId = getProductId(window.location.href) + console.log("[useProductPageState] Using productId:", productId) if (productId) { try { - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:97", - message: "Effect started", - data: { productId, currentProductId }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H3" - }) - } - ).catch(() => {}) - // #endregion // Check 1: Global snooze console.log("[useProductPageState] Checking for global snooze...") const snoozedUntil = await storage.getGlobalSnooze() - // #region agent log - const nowTime = Date.now() - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:101", - message: "Snooze check", - data: { - snoozedUntil, - nowTime, - isActive: snoozedUntil && snoozedUntil > nowTime, - diff: snoozedUntil ? snoozedUntil - nowTime : null - }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H1,H5" - }) - } - ).catch(() => {}) - // #endregion if (snoozedUntil && snoozedUntil > Date.now()) { console.log( "[useProductPageState] Global snooze active until:", new Date(snoozedUntil) ) - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:106", - message: "Snooze ACTIVE - hiding overlay", - data: { snoozedUntil, now: Date.now() }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H1,H5" - }) - } - ).catch(() => {}) - // #endregion setPluginClosedState(true) setCurrentView(null) + setProduct(null) return } else if (snoozedUntil) { // Snooze has expired - clear it console.log("[useProductPageState] Snooze expired, clearing...") - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:112", - message: "Snooze EXPIRED - clearing", - data: { snoozedUntil, now: Date.now() }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H1" - }) - } - ).catch(() => {}) - // #endregion await storage.clearGlobalSnooze() - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:112-after", - message: "After clearGlobalSnooze", - data: {}, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H1" - }) - } - ).catch(() => {}) - // #endregion } - // Check 2: Product has terminal state (I_NEED_THIS) + // Check 2: Product has terminal state (I_NEED_THIS only) const compositeKey = `${marketplace}-${productId}` console.log( "[useProductPageState] Checking product storage for key:", @@ -220,51 +110,19 @@ export function useProductPageState({ ) setPluginClosedState(true) setCurrentView(null) + setProduct(null) return } // Check 3: User global close const globalClosed = await storage.getGlobalPluginClosed() - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:136", - message: "Global plugin closed check", - data: { globalClosed }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H2" - }) - } - ).catch(() => {}) - // #endregion if (globalClosed) { console.log( "[useProductPageState] Global plugin closed - hiding overlay" ) - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:141", - message: "Global CLOSED - hiding overlay", - data: { globalClosed }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H2" - }) - } - ).catch(() => {}) - // #endregion setPluginClosedState(true) setCurrentView(null) + setProduct(null) return } @@ -272,41 +130,26 @@ export function useProductPageState({ console.log( "[useProductPageState] All checks passed - showing overlay" ) - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:150", - message: "ALL CHECKS PASSED - showing overlay", - data: { willSetPluginClosedTo: false }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H4" - }) - } - ).catch(() => {}) - // #endregion setPluginClosedState(false) - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:150-after", - message: "After setPluginClosedState(false)", - data: {}, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H4" - }) - } - ).catch(() => {}) - // #endregion + + // Extract and merge product (fresh DOM data + stored state) + try { + const freshProduct = extractProduct(marketplace, productId) + const mergedProduct = existingProduct + ? { ...freshProduct, state: existingProduct.state } + : freshProduct + setProduct(mergedProduct) + console.log( + "[useProductPageState] Product extracted and merged:", + mergedProduct.id + ) + } catch (error) { + console.error( + "[useProductPageState] Failed to extract product:", + error + ) + setProduct(null) + } // Check for pending reminders (early return / old flame views) const reminders = await storage.getReminders() @@ -338,15 +181,15 @@ export function useProductPageState({ setReminderDuration(null) setReminderStartTime(null) setCurrentView(null) + // Keep product - it was already extracted above return } + setReminderId(pendingReminder.id) setReminderDuration(pendingReminder.duration) setReminderStartTime( pendingReminder.reminderTime - pendingReminder.duration ) - const product = extractProduct(marketplace, productId) - setCurrentProduct(product) const now = Date.now() if (pendingReminder.reminderTime > now) { @@ -392,26 +235,6 @@ export function useProductPageState({ console.log( "[useProductPageState] Snooze storage changed, re-checking..." ) - // #region agent log - fetch( - "http://127.0.0.1:7242/ingest/1d41934a-9eab-419c-a72a-f8274ce160e8", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - location: "useProductPageState.ts:232", - message: "Snooze storage CHANGED - listener triggered", - data: { - oldValue: changes["thinktwice_snooze"]?.oldValue, - newValue: changes["thinktwice_snooze"]?.newValue - }, - timestamp: Date.now(), - sessionId: "debug-session", - hypothesisId: "H1" - }) - } - ).catch(() => {}) - // #endregion checkForPendingReminder() } @@ -432,7 +255,7 @@ export function useProductPageState({ StorageProxyService.addChangeListener(storageListener) return removeListener - }, [getProductId, marketplace, tabIdSession, currentProductId]) + }, [getProductId, marketplace, tabIdSession, urlChangeCounter]) const setPluginClosed = async (closed: boolean) => { console.log("[useProductPageState] Setting global plugin closed:", closed) @@ -442,7 +265,7 @@ export function useProductPageState({ return { currentView, - currentProduct, + product, reminderId, reminderDuration, reminderStartTime, diff --git a/tests/e2e/close-flow.spec.ts b/tests/e2e/close-flow.spec.ts index cda723a..ce9d891 100644 --- a/tests/e2e/close-flow.spec.ts +++ b/tests/e2e/close-flow.spec.ts @@ -1,7 +1,8 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" +import { SECONDARY_PRODUCT_ID } from "./test-config" test.describe('ThinkTwice "Close and Pause" Flow', () => { test('should handle "Close for now" (Global Close)', async ({ @@ -10,12 +11,13 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -46,7 +48,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { // Open a NEW tab to the same product const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(newPage, productId) // Verify overlay is NOT visible in the new tab (global close affects all tabs) const newOverlayPage = new OverlayPage(newPage, extensionId) @@ -59,12 +61,14 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId1) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -86,7 +90,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { // Navigate to a DIFFERENT product page in a NEW tab const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.SECONDARY) + await navigateToProduct(newPage, productId2) // Verify overlay is NOT shown (global snooze active) const newOverlayPage = new OverlayPage(newPage, extensionId) @@ -99,12 +103,13 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -138,12 +143,14 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { test.setTimeout(70000) const page = await extensionContext.newPage() + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() // Navigate to Amazon product page - await navigateToProduct(page, "B005EJH6Z4") + await navigateToProduct(page, productId1) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -167,7 +174,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { // Open a NEW tab immediately console.log("Opening new tab to verify global snooze...") const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.SECONDARY) + await navigateToProduct(newPage, productId2) // Verify overlay is NOT visible in the new tab (global snooze active) const newOverlayPage = new OverlayPage(newPage, extensionId) diff --git a/tests/e2e/extension.spec.ts b/tests/e2e/extension.spec.ts index 9c98957..2f36d3d 100644 --- a/tests/e2e/extension.spec.ts +++ b/tests/e2e/extension.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" test.describe("ThinkTwice Plugin E2E", () => { @@ -20,7 +20,7 @@ test.describe("ThinkTwice Plugin E2E", () => { // Navigate to a known valid Amazon product page console.log("Navigating to Amazon product page...") - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, PRIMARY_PRODUCT_ID) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts index 4db60dd..a3f6977 100644 --- a/tests/e2e/fixtures.ts +++ b/tests/e2e/fixtures.ts @@ -37,8 +37,9 @@ export const test = base.extend({ "../../tmp/test-user-data-" + Math.random().toString(36).substring(7) ) + const isHeaded = process.env.HEADED === 'true' const context = await chromium.launchPersistentContext(userDataDir, { - headless: !!process.env.CI || process.env.HEADLESS === "true", + headless: !isHeaded, // Default headless, set HEADED=true for headed mode userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", locale: "en-US", @@ -51,7 +52,10 @@ export const test = base.extend({ "--disable-dev-shm-usage", "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins,site-per-process", - "--disable-site-isolation-trials" + "--disable-site-isolation-trials", + "--window-size=1280,720", + "--disable-web-security", + "--disable-features=VizDisplayCompositor" ] }) diff --git a/tests/e2e/idontneedit.spec.ts b/tests/e2e/idontneedit.spec.ts index 59ff1e3..2fedca8 100644 --- a/tests/e2e/idontneedit.spec.ts +++ b/tests/e2e/idontneedit.spec.ts @@ -1,6 +1,6 @@ -import { test } from "./fixtures" +import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { TEST_CONFIG, PRIMARY_PRODUCT_ID } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { buildProductId } from "./utils/product-helpers" @@ -11,12 +11,13 @@ test.describe('ThinkTwice "I don\'t really need it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to a known valid Amazon product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -33,18 +34,21 @@ test.describe('ThinkTwice "I don\'t really need it" Flow', () => { ) // Verify product state is saved to storage - const expectedProductId = buildProductId( - "amazon", - TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY - ) + const expectedProductId = buildProductId("amazon", productId) await extensionHelper.assertProductState( expectedProductId, TEST_CONFIG.STATES.DONT_NEED_IT ) - // Verify it auto-closes after roughly 4 seconds - await overlayPage.expectCelebrationHidden( - TEST_CONFIG.TEXT.CELEBRATION_DONT_NEED - ) + // Verify tab closes or overlay disappears after celebration (4 seconds) + try { + await page.waitForTimeout(5000) + if (!page.isClosed()) { + await overlayPage.expectHidden(2000) + } + } catch { + expect(page.isClosed()).toBe(true) + console.log("[Test] Tab was closed as expected") + } }) }) diff --git a/tests/e2e/ineedit.spec.ts b/tests/e2e/ineedit.spec.ts index da95e09..ccd3bc5 100644 --- a/tests/e2e/ineedit.spec.ts +++ b/tests/e2e/ineedit.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID, SECONDARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { buildProductId } from "./utils/product-helpers" @@ -11,12 +11,13 @@ test.describe('ThinkTwice "I need it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -45,10 +46,7 @@ test.describe('ThinkTwice "I need it" Flow', () => { await overlayPage.expectHidden(2000) // Verify product state is saved to storage - const expectedProductId = buildProductId( - "amazon", - TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY - ) + const expectedProductId = buildProductId("amazon", productId) await extensionHelper.assertProductState( expectedProductId, TEST_CONFIG.STATES.I_NEED_THIS @@ -59,7 +57,7 @@ test.describe('ThinkTwice "I need it" Flow', () => { // Navigate to the same product URL again const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(newPage, productId) // Wait a bit for the extension to check the product state await newPage.waitForTimeout(2000) @@ -75,12 +73,14 @@ test.describe('ThinkTwice "I need it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() - // Navigate to first product (PRIMARY) - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + // Navigate to first product + await navigateToProduct(page, productId1) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -105,8 +105,8 @@ test.describe('ThinkTwice "I need it" Flow', () => { // Verify overlay is hidden for Product A after auto-close await overlayPage.expectHidden(2000) - // Navigate to a different product (SECONDARY) - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.SECONDARY) + // Navigate to a different product + await navigateToProduct(page, productId2) // Wait for extension to initialize after navigation await page.waitForTimeout(1000) diff --git a/tests/e2e/page-objects/OverlayPage.ts b/tests/e2e/page-objects/OverlayPage.ts index bc85e0f..47cc9e8 100644 --- a/tests/e2e/page-objects/OverlayPage.ts +++ b/tests/e2e/page-objects/OverlayPage.ts @@ -112,6 +112,17 @@ export class OverlayPage { await button.click() } + /** + * Click any button by name + */ + async clickButton(name: string | RegExp): Promise { + const button = this.getButton(name) + await expect(button).toBeVisible({ + timeout: TEST_CONFIG.TIMEOUTS.BUTTON_VISIBLE + }) + await button.click() + } + /** * Wait for overlay to be attached to the DOM */ diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts index de23d1b..a96b4f9 100644 --- a/tests/e2e/setup.ts +++ b/tests/e2e/setup.ts @@ -1,7 +1,7 @@ import path from "path" import { chromium, expect, test as setup } from "@playwright/test" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID } from "./test-config" const EXTENSION_PATH = path.resolve(__dirname, "../../build/chrome-mv3-dev") @@ -9,8 +9,9 @@ setup("Verify extension is loadable", async () => { console.log("Setup: Verifying extension load from", EXTENSION_PATH) const userDataDir = path.join(__dirname, "../../tmp/test-user-data-setup") + const isHeaded = process.env.HEADED === 'true' const context = await chromium.launchPersistentContext(userDataDir, { - headless: process.env.CI ? true : false, // Headless in CI, headed locally for debugging + headless: !isHeaded, // Default headless, set HEADED=true for headed mode userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", viewport: { width: 1280, height: 720 }, @@ -24,7 +25,10 @@ setup("Verify extension is loadable", async () => { "--disable-dev-shm-usage", "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins,site-per-process", - "--disable-site-isolation-trials" + "--disable-site-isolation-trials", + "--window-size=1280,720", + "--disable-web-security", + "--disable-features=VizDisplayCompositor" ] }) @@ -71,7 +75,7 @@ setup("Verify extension is loadable", async () => { }) await page.goto( - `https://www.amazon.com/dp/${TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY}`, + `https://www.amazon.com/dp/${PRIMARY_PRODUCT_ID}`, { waitUntil: "load" } ) diff --git a/tests/e2e/sleeponit.spec.ts b/tests/e2e/sleeponit.spec.ts index 3a1b08a..3f2ce3e 100644 --- a/tests/e2e/sleeponit.spec.ts +++ b/tests/e2e/sleeponit.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" import { PopupPage } from "./page-objects/PopupPage" -import { TEST_CONFIG } from "./test-config" +import { PRIMARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { extractProductInfo } from "./utils/product-helpers" @@ -12,12 +12,13 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Create overlay page object const overlayPage = new OverlayPage(page, extensionId) @@ -73,12 +74,13 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { test.setTimeout(120000) // 2 minutes const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Extract product information const productInfo = await extractProductInfo(page) @@ -156,12 +158,13 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID // Clear storage to ensure clean state await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, productId) // Extract product information const productInfo = await extractProductInfo(page) @@ -208,20 +211,20 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { await productPage.waitForLoadState("load") // Verify we're on the correct product page - expect(productPage.url()).toContain(TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) - console.log("[Test] Product page opened successfully") + expect(productPage.url()).toContain(productId) + console.log("[Test] Product page opened successfully") - // Reload the popup to see updated state - await popupPageObj.reload() + // Reload the popup to see updated state + await popupPageObj.reload() - // Verify the product is NO LONGER in "Sleeping on it" section - if (productInfo.name) { - await popupPageObj.expectProductNotInSleeping(productInfo.name) - } + // Verify the product is NO LONGER in "Sleeping on it" section + if (productInfo.name) { + await popupPageObj.expectProductNotInSleeping(productInfo.name) + } - console.log( - '[Test] Successfully verified "I changed my mind" flow - product removed from sleeping section' - ) + console.log( + '[Test] Successfully verified "I changed my mind" flow - product removed from sleeping section' + ) // Clean up await productPage.close() @@ -238,7 +241,7 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { await extensionHelper.clearStorage() // Navigate to the test product page - await navigateToProduct(page, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(page, PRIMARY_PRODUCT_ID) // Create overlay page object and complete sleep on it flow const overlayPage = new OverlayPage(page, extensionId) @@ -260,7 +263,7 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { // Go back to the same product url const newPage = await extensionContext.newPage() - await navigateToProduct(newPage, TEST_CONFIG.AMAZON_PRODUCT_IDS.PRIMARY) + await navigateToProduct(newPage, PRIMARY_PRODUCT_ID) // Create overlay page object for the new page const newOverlayPage = new OverlayPage(newPage, extensionId) @@ -279,4 +282,361 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { await newPage.close() }) + + test("should show BackToAnOldFlame view when returning after timer expires", async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + test.setTimeout(120000) // 2 minutes + + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Create overlay page object and complete sleep on it flow with 1 minute timer + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/1 minute/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Wait for timer to expire + console.log("[Test] Waiting for 1 minute timer to expire...") + await page.waitForTimeout(TEST_CONFIG.TIMEOUTS.REMINDER_1MIN) + + // Go back to the same product url AFTER timer expired + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify BackToAnOldFlame view is shown + await newOverlayPage.expectTextVisible("You're back!", 5000) + await newOverlayPage.expectTextVisible("thoughtful", 5000) + + // Verify buttons are present + await expect(newOverlayPage.getButton(/Yes, I want it/i)).toBeVisible() + await expect(newOverlayPage.getButton(/I don't need it/i)).toBeVisible() + await expect(newOverlayPage.getButton(/I'm still not sure/i)).toBeVisible() + + await newPage.close() + }) + + test('should delete reminder when clicking "I need this now" during early return', async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Extract product information + const productInfo = await extractProductInfo(page) + + // Create overlay page object and complete sleep on it flow + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/24 hours/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Open popup and verify product is in "Sleeping on it" section + const popupPageObj = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj.goto() + if (productInfo.name) { + await popupPageObj.expectProductInSleeping(productInfo.name) + } + await popupPageObj.page.close() + + // Go back to the same product url BEFORE timer expires + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify EarlyReturnFromSleep view is shown + await newOverlayPage.expectTextVisible("You're back!", 5000) + + // Click "I need this now" button + await newOverlayPage.clickButton(/I need this now/i) + + // Verify celebration is shown + await newOverlayPage.expectCelebrationVisible( + TEST_CONFIG.TEXT.CELEBRATION_NEED_IT + ) + + // Wait for overlay to hide + await newOverlayPage.expectHidden(TEST_CONFIG.TIMEOUTS.CELEBRATION_FADE) + + // Open popup again and verify product is NOT in "Sleeping on it" section + const popupPageObj2 = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj2.goto() + if (productInfo.name) { + await popupPageObj2.expectProductNotInSleeping(productInfo.name) + } + + console.log( + '[Test] Successfully verified reminder deletion after "I need this now"' + ) + }) + + test('should delete reminder when clicking "Yes, I want it" after timer expires', async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + test.setTimeout(120000) // 2 minutes + + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Extract product information + const productInfo = await extractProductInfo(page) + + // Create overlay page object and complete sleep on it flow with 1 minute timer + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/1 minute/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Wait for timer to expire + console.log("[Test] Waiting for 1 minute timer to expire...") + await page.waitForTimeout(TEST_CONFIG.TIMEOUTS.REMINDER_1MIN) + + // Open popup and verify product is still in "Sleeping on it" section + const popupPageObj = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj.goto() + if (productInfo.name) { + await popupPageObj.expectProductInSleeping(productInfo.name) + } + await popupPageObj.page.close() + + // Go back to the same product url AFTER timer expired + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify BackToAnOldFlame view is shown + await newOverlayPage.expectTextVisible("You're back!", 5000) + + // Click "Yes, I want it" button + await newOverlayPage.clickButton(/Yes, I want it/i) + + // Verify celebration is shown + await newOverlayPage.expectCelebrationVisible( + TEST_CONFIG.TEXT.CELEBRATION_NEED_IT + ) + + // Wait for overlay to hide + await newOverlayPage.expectHidden(TEST_CONFIG.TIMEOUTS.CELEBRATION_FADE) + + // Open popup again and verify product is NOT in "Sleeping on it" section + const popupPageObj2 = new PopupPage( + await extensionContext.newPage(), + extensionId + ) + await popupPageObj2.goto() + if (productInfo.name) { + await popupPageObj2.expectProductNotInSleeping(productInfo.name) + } + + console.log( + '[Test] Successfully verified reminder deletion after "Yes, I want it"' + ) + }) + + test("should show EarlyReturnFromSleep on multiple early returns", async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Create overlay page object and complete sleep on it flow + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/24 hours/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // First early return + const newPage1 = await extensionContext.newPage() + await navigateToProduct(newPage1, productId) + const newOverlayPage1 = new OverlayPage(newPage1, extensionId) + await newOverlayPage1.expectAttached() + await newOverlayPage1.expectTextVisible("You're back!", 5000) + await expect(newOverlayPage1.getButton(/I need this now/i)).toBeVisible() + + // Click "I'll wait" + await newOverlayPage1.clickButton(/I'll wait/i) + await newOverlayPage1.expectHidden(5000) + await newPage1.close() + + // Second early return - should still show EarlyReturnFromSleep + const newPage2 = await extensionContext.newPage() + await navigateToProduct(newPage2, productId) + const newOverlayPage2 = new OverlayPage(newPage2, extensionId) + await newOverlayPage2.expectAttached() + await newOverlayPage2.expectTextVisible("You're back!", 5000) + await expect(newOverlayPage2.getButton(/I need this now/i)).toBeVisible() + + console.log( + "[Test] Successfully verified multiple early returns show EarlyReturnFromSleep" + ) + + await newPage2.close() + }) + + test("should show BackToAnOldFlame at exact expiration time", async ({ + extensionContext, + extensionId, + extensionHelper + }) => { + test.setTimeout(120000) // 2 minutes + + const page = await extensionContext.newPage() + const productId = PRIMARY_PRODUCT_ID + + // Clear storage to ensure clean state + await extensionHelper.clearStorage() + + // Navigate to the test product page + await navigateToProduct(page, productId) + + // Record start time + const startTime = Date.now() + + // Create overlay page object and complete sleep on it flow with 1 minute timer + const overlayPage = new OverlayPage(page, extensionId) + await overlayPage.expectAttached() + await overlayPage.clickSleepOnIt() + await overlayPage.selectDuration(/1 minute/i) + await overlayPage.clickSetReminder() + await overlayPage.expectSuccessMessage() + + // Wait for tab to close + try { + await page.waitForTimeout(6000) + if (!page.isClosed()) { + await page.close() + } + } catch { + // Expected: page closed + } + + // Wait until exactly 1 minute has passed + const elapsedTime = Date.now() - startTime + const remainingTime = TEST_CONFIG.TIMEOUTS.REMINDER_1MIN - elapsedTime + if (remainingTime > 0) { + await page.waitForTimeout(remainingTime) + } + + // Go back to the same product url at expiration time + const newPage = await extensionContext.newPage() + await navigateToProduct(newPage, productId) + + // Create overlay page object for the new page + const newOverlayPage = new OverlayPage(newPage, extensionId) + await newOverlayPage.expectAttached() + + // Verify BackToAnOldFlame view is shown (not EarlyReturnFromSleep) + await newOverlayPage.expectTextVisible("You're back!", 5000) + await newOverlayPage.expectTextVisible("thoughtful", 5000) + + // Verify buttons are for BackToAnOldFlame + await expect(newOverlayPage.getButton(/Yes, I want it/i)).toBeVisible() + await expect(newOverlayPage.getButton(/I'm still not sure/i)).toBeVisible() + + console.log( + "[Test] Successfully verified BackToAnOldFlame at exact expiration time" + ) + + await newPage.close() + }) }) diff --git a/tests/e2e/test-config.ts b/tests/e2e/test-config.ts index aa083f4..6a40a4b 100644 --- a/tests/e2e/test-config.ts +++ b/tests/e2e/test-config.ts @@ -15,19 +15,7 @@ export const TEST_CONFIG = { * Amazon product IDs used across E2E tests. * These should be stable, long-lived products. */ - AMAZON_PRODUCT_IDS: { - /** - * Primary test product - used in most test cases - * Current: Anker PowerCore 10000 Portable Charger - */ - PRIMARY: "B005EJH6Z4", - - /** - * Secondary test product - used for multi-tab scenarios - * Should be different from PRIMARY to test different products - */ - SECONDARY: "B07BMKXBVW" - }, + AMAZON_PRODUCT_IDS: ["B005EJH6Z4", "B07BMKXBVW", "B09B8V1LZ3", "B0F33VR8LB"] as const, /** * Timeout values in milliseconds @@ -83,5 +71,18 @@ export const TEST_CONFIG = { I_NEED_THIS: "iNeedThis", DONT_NEED_IT: "dontNeedIt", SLEEPING: "sleeping" - } + }, } as const + +/** + * Get a random product ID from the configured product IDs + */ +function getRandomProductId(): string { + const ids = TEST_CONFIG.AMAZON_PRODUCT_IDS + const idx = Math.floor(Math.random() * ids.length) + return ids[idx] +} + + +export const PRIMARY_PRODUCT_ID = getRandomProductId() +export const SECONDARY_PRODUCT_ID = getRandomProductId() \ No newline at end of file diff --git a/tests/flows/Idontneedit-flow.md b/tests/flows/Idontneedit-flow.md index bf91d23..c20daff 100644 --- a/tests/flows/Idontneedit-flow.md +++ b/tests/flows/Idontneedit-flow.md @@ -39,11 +39,10 @@ The "I don't really need it" action marks the product as unwanted, records this ### 5. Auto-Close -- **Timer**: After 4 seconds, the `Celebration` component triggers its `onClose` callback. -- **Cleanup**: - - `setPluginClosed(true)` is called. - - The tab session state is updated in storage to mark `pluginClosed: true`. -- **Result**: The extension overlay is removed from the DOM (`shouldShowOverlay` becomes `false`), effectively ending the interaction for that product/session. +- **Timer**: After 4 seconds, the celebration triggers tab closure. +- **Action**: Calls `ChromeMessaging.closeCurrentTab()`. +- **Fallback**: If tab close fails, calls `onClose()` to hide the overlay. +- **Result**: The browser tab closes, removing the temptation to purchase. If the user navigates to the same product again in the future, the overlay will appear again (unlike "I need it" which is a terminal state). ## Related Files diff --git a/views/BackToAnOldFlame.tsx b/views/BackToAnOldFlame.tsx index a3b944d..5d499d0 100644 --- a/views/BackToAnOldFlame.tsx +++ b/views/BackToAnOldFlame.tsx @@ -1,3 +1,17 @@ +/** + * BackToAnOldFlame View + * + * Shown when a user returns to a product page AFTER the "Sleep on it" reminder timer has expired. + * This indicates they successfully waited the full duration and are now thoughtfully reconsidering. + * + * Scenario: User clicked "Sleep on it", reminder expired (e.g., after 24 hours), and user came back. + * + * Key differences from EarlyReturnFromSleep: + * - Uses "Thoughtful" icon (vs Clock icon) + * - Messaging emphasizes successful waiting and thoughtful decision-making + * - Button options: "Yes, I want it" / "I don't need it" / "I'm still not sure" + */ + import { useState } from "react" import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg" diff --git a/views/EarlyReturnFromSleep.tsx b/views/EarlyReturnFromSleep.tsx index 3105e79..e25397d 100644 --- a/views/EarlyReturnFromSleep.tsx +++ b/views/EarlyReturnFromSleep.tsx @@ -1,3 +1,17 @@ +/** + * EarlyReturnFromSleep View + * + * Shown when a user returns to a product page BEFORE the "Sleep on it" reminder timer has expired. + * This indicates they couldn't resist and came back early, breaking their commitment to wait. + * + * Scenario: User clicked "Sleep on it" (e.g., 24 hours), but came back after only 2 hours. + * + * Key differences from BackToAnOldFlame: + * - Uses "Clock" icon (vs Thoughtful icon) + * - Messaging emphasizes timing and encourages continuing to wait + * - Button options: "I need this now" / "I'll wait" / "I don't need it" + */ + import { useState } from "react" import clockIcon from "url:../assets/icons/Icons/Clock.svg" diff --git a/views/IDontNeedIt.tsx b/views/IDontNeedIt.tsx index 70e9e84..64877df 100644 --- a/views/IDontNeedIt.tsx +++ b/views/IDontNeedIt.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react" import lightbulbIcon from "url:../assets/icons/Icons/Lightbulb.svg" import starIcon from "url:../assets/icons/Icons/Star.svg" import trophyIcon from "url:../assets/icons/Icons/Trophy.svg" @@ -7,6 +8,7 @@ import Card from "../components/ui/Card" import Header from "../components/ui/Header" import PrivacyBadge from "../components/ui/PrivacyBadge" import { commonSpacing, iconSize, layout, typography } from "../design-system" +import { ChromeMessaging } from "../services/ChromeMessaging" import Celebration from "./Celebration" // Feature flag: Set to true to show investment options view @@ -34,6 +36,20 @@ const optionsContainerStyle: React.CSSProperties = { } const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { + // Handle tab close after celebration + const handleCelebrationClose = async () => { + console.log("[IDontNeedIt] Requesting tab close...") + try { + await ChromeMessaging.closeCurrentTab() + console.log("[IDontNeedIt] Tab close successful") + } catch (error) { + console.error("[IDontNeedIt] Tab close failed:", error) + if (onClose) { + onClose() + } + } + } + // When feature flag is disabled, show celebration instead of investment options if (!SHOW_INVESTMENT_OPTIONS) { return ( @@ -44,7 +60,7 @@ const IDontNeedIt = ({ onBack, onClose }: IDontNeedItProps) => { subtitle="Your future self will thank you for being so thoughtful." autoCloseDelay={4000} onBack={onBack} - onClose={onClose} + onClose={handleCelebrationClose} /> ) } diff --git a/views/ProductView.tsx b/views/ProductView.tsx index 7908710..486f418 100644 --- a/views/ProductView.tsx +++ b/views/ProductView.tsx @@ -19,12 +19,10 @@ import { } from "../design-system" import { ProductActionManager } from "../managers/ProductActionManager" import type { Product } from "../storage" -import { storage } from "../storage" -import { extractProduct } from "../utils/productExtractor" +import { ProductState, storage } from "../storage" type ProductViewProps = { - productId?: string | null - marketplace: "amazon" + product: Product | null onShowIDontNeedIt: (product: Product | null) => void onShowSleepOnIt: (product: Product | null) => void onShowINeedIt: () => void @@ -71,48 +69,34 @@ const actionsGroupStyle: React.CSSProperties = { } const ProductView = ({ - productId, - marketplace, + product, onShowIDontNeedIt, onShowSleepOnIt, onShowINeedIt, onClose }: ProductViewProps) => { - const [extractedProduct, setExtractedProduct] = useState(null) const [showPauseMenu, setShowPauseMenu] = useState(false) const [showPrivacyInfo, setShowPrivacyInfo] = useState(false) - useEffect(() => { - if (productId) { - try { - const product = extractProduct(marketplace, productId) - setExtractedProduct(product) - } catch (error) { - console.error("[ProductView] Failed to extract product:", error) - setExtractedProduct(null) - } - } - }, [productId, marketplace]) - const handleSleepOnIt = () => { - onShowSleepOnIt(extractedProduct) + onShowSleepOnIt(product) } const handleIDontNeedIt = async () => { - if (extractedProduct) { + if (product) { try { - await ProductActionManager.dontNeedIt(extractedProduct) + await ProductActionManager.dontNeedIt(product) } catch (error) { console.error("[ProductView] Failed to execute dontNeedIt:", error) } } - onShowIDontNeedIt(extractedProduct) + onShowIDontNeedIt(product) } const handleINeedIt = async () => { - if (extractedProduct) { + if (product) { try { - await ProductActionManager.needIt(extractedProduct) + await ProductActionManager.needIt(product) } catch (error) { console.error("[ProductView] Failed to execute needIt:", error) } @@ -176,6 +160,19 @@ const ProductView = ({ setShowPrivacyInfo(!showPrivacyInfo) } + const handleIntroSentect = (product: Product | null): string => { + console.log("[DEBUG] PRODUCT ---->", product) + if (!product) { + return "" + } + + if (product.state && product.state === ProductState.DONT_NEED_IT) { + return "You are back! Are you thinking about buying this product?" + } + + return "Quick thought before you buy" + } + return ( }>

- Quick thought before you buy + {handleIntroSentect(product)}

From d90d82de7bdf967a0b1a143d4e82047f4565897b Mon Sep 17 00:00:00 2001 From: verte Date: Fri, 30 Jan 2026 10:57:57 -0500 Subject: [PATCH 5/5] style: fix linting errors and formatting issues - Remove unused ProductActionManager import from amazon.tsx - Prefix unused parameters with underscore - Remove unused React imports from views - Format test files with Prettier --- contents/amazon.tsx | 5 ++--- tests/e2e/close-flow.spec.ts | 13 ++++++++----- tests/e2e/fixtures.ts | 2 +- tests/e2e/idontneedit.spec.ts | 2 +- tests/e2e/ineedit.spec.ts | 6 +++++- tests/e2e/setup.ts | 9 ++++----- tests/e2e/sleeponit.spec.ts | 20 ++++++++++---------- tests/e2e/test-config.ts | 12 ++++++++---- views/IDontNeedIt.tsx | 2 +- views/ProductView.tsx | 2 +- 10 files changed, 41 insertions(+), 32 deletions(-) diff --git a/contents/amazon.tsx b/contents/amazon.tsx index 742d1ca..c0becb8 100644 --- a/contents/amazon.tsx +++ b/contents/amazon.tsx @@ -8,7 +8,6 @@ import { useState } from "react" import { useGoogleFonts } from "~/hooks/useGoogleFonts" import { useProductPageState } from "~/hooks/useProductPageState" -import { ProductActionManager } from "~/managers/ProductActionManager" import type { Product } from "~/storage" import BackToAnOldFlame from "~/views/BackToAnOldFlame" import CelebrateThoughtfulPurchase from "~/views/CelebrateThoughtfulPurchase" @@ -91,7 +90,7 @@ const App = () => { setLocalView(VIEW.PRODUCT) } - const handleShowIDontNeedIt = (product: Product | null) => { + const handleShowIDontNeedIt = (_product: Product | null) => { setLocalView(VIEW.I_DONT_NEED_IT) } @@ -124,7 +123,7 @@ const App = () => { if (regex.test(window.location.href)) { // Extract product ID from URL const url = window.location.href - const productId = getAmazonProductId(url) + const _productId = getAmazonProductId(url) if (currentView === VIEW.EARLY_RETURN) { return ( diff --git a/tests/e2e/close-flow.spec.ts b/tests/e2e/close-flow.spec.ts index ce9d891..0e540d1 100644 --- a/tests/e2e/close-flow.spec.ts +++ b/tests/e2e/close-flow.spec.ts @@ -1,8 +1,11 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { PRIMARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" +import { + PRIMARY_PRODUCT_ID, + SECONDARY_PRODUCT_ID, + TEST_CONFIG +} from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" -import { SECONDARY_PRODUCT_ID } from "./test-config" test.describe('ThinkTwice "Close and Pause" Flow', () => { test('should handle "Close for now" (Global Close)', async ({ @@ -11,7 +14,7 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { extensionHelper }) => { const page = await extensionContext.newPage() - const productId = PRIMARY_PRODUCT_ID + const productId = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() @@ -143,8 +146,8 @@ test.describe('ThinkTwice "Close and Pause" Flow', () => { test.setTimeout(70000) const page = await extensionContext.newPage() - const productId1 = SECONDARY_PRODUCT_ID - const productId2 = PRIMARY_PRODUCT_ID + const productId1 = SECONDARY_PRODUCT_ID + const productId2 = PRIMARY_PRODUCT_ID // Clear storage await extensionHelper.clearStorage() diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts index a3f6977..500a208 100644 --- a/tests/e2e/fixtures.ts +++ b/tests/e2e/fixtures.ts @@ -37,7 +37,7 @@ export const test = base.extend({ "../../tmp/test-user-data-" + Math.random().toString(36).substring(7) ) - const isHeaded = process.env.HEADED === 'true' + const isHeaded = process.env.HEADED === "true" const context = await chromium.launchPersistentContext(userDataDir, { headless: !isHeaded, // Default headless, set HEADED=true for headed mode userAgent: diff --git a/tests/e2e/idontneedit.spec.ts b/tests/e2e/idontneedit.spec.ts index 2fedca8..921bfce 100644 --- a/tests/e2e/idontneedit.spec.ts +++ b/tests/e2e/idontneedit.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { TEST_CONFIG, PRIMARY_PRODUCT_ID } from "./test-config" +import { PRIMARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { buildProductId } from "./utils/product-helpers" diff --git a/tests/e2e/ineedit.spec.ts b/tests/e2e/ineedit.spec.ts index ccd3bc5..438b5ff 100644 --- a/tests/e2e/ineedit.spec.ts +++ b/tests/e2e/ineedit.spec.ts @@ -1,6 +1,10 @@ import { expect, test } from "./fixtures" import { OverlayPage } from "./page-objects/OverlayPage" -import { PRIMARY_PRODUCT_ID, SECONDARY_PRODUCT_ID, TEST_CONFIG } from "./test-config" +import { + PRIMARY_PRODUCT_ID, + SECONDARY_PRODUCT_ID, + TEST_CONFIG +} from "./test-config" import { navigateToProduct } from "./utils/extension-helpers" import { buildProductId } from "./utils/product-helpers" diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts index a96b4f9..597eaa2 100644 --- a/tests/e2e/setup.ts +++ b/tests/e2e/setup.ts @@ -9,7 +9,7 @@ setup("Verify extension is loadable", async () => { console.log("Setup: Verifying extension load from", EXTENSION_PATH) const userDataDir = path.join(__dirname, "../../tmp/test-user-data-setup") - const isHeaded = process.env.HEADED === 'true' + const isHeaded = process.env.HEADED === "true" const context = await chromium.launchPersistentContext(userDataDir, { headless: !isHeaded, // Default headless, set HEADED=true for headed mode userAgent: @@ -74,10 +74,9 @@ setup("Verify extension is loadable", async () => { "sec-ch-ua-platform": '"Windows"' }) - await page.goto( - `https://www.amazon.com/dp/${PRIMARY_PRODUCT_ID}`, - { waitUntil: "load" } - ) + await page.goto(`https://www.amazon.com/dp/${PRIMARY_PRODUCT_ID}`, { + waitUntil: "load" + }) // Plasmo injects a custom element with the tag 'plasmo-csui' // We'll wait for this standard selector diff --git a/tests/e2e/sleeponit.spec.ts b/tests/e2e/sleeponit.spec.ts index 3f2ce3e..529b973 100644 --- a/tests/e2e/sleeponit.spec.ts +++ b/tests/e2e/sleeponit.spec.ts @@ -212,19 +212,19 @@ test.describe('ThinkTwice "Sleep on it" Flow', () => { // Verify we're on the correct product page expect(productPage.url()).toContain(productId) - console.log("[Test] Product page opened successfully") + console.log("[Test] Product page opened successfully") - // Reload the popup to see updated state - await popupPageObj.reload() + // Reload the popup to see updated state + await popupPageObj.reload() - // Verify the product is NO LONGER in "Sleeping on it" section - if (productInfo.name) { - await popupPageObj.expectProductNotInSleeping(productInfo.name) - } + // Verify the product is NO LONGER in "Sleeping on it" section + if (productInfo.name) { + await popupPageObj.expectProductNotInSleeping(productInfo.name) + } - console.log( - '[Test] Successfully verified "I changed my mind" flow - product removed from sleeping section' - ) + console.log( + '[Test] Successfully verified "I changed my mind" flow - product removed from sleeping section' + ) // Clean up await productPage.close() diff --git a/tests/e2e/test-config.ts b/tests/e2e/test-config.ts index 6a40a4b..d18fd49 100644 --- a/tests/e2e/test-config.ts +++ b/tests/e2e/test-config.ts @@ -15,7 +15,12 @@ export const TEST_CONFIG = { * Amazon product IDs used across E2E tests. * These should be stable, long-lived products. */ - AMAZON_PRODUCT_IDS: ["B005EJH6Z4", "B07BMKXBVW", "B09B8V1LZ3", "B0F33VR8LB"] as const, + AMAZON_PRODUCT_IDS: [ + "B005EJH6Z4", + "B07BMKXBVW", + "B09B8V1LZ3", + "B0F33VR8LB" + ] as const, /** * Timeout values in milliseconds @@ -71,7 +76,7 @@ export const TEST_CONFIG = { I_NEED_THIS: "iNeedThis", DONT_NEED_IT: "dontNeedIt", SLEEPING: "sleeping" - }, + } } as const /** @@ -83,6 +88,5 @@ function getRandomProductId(): string { return ids[idx] } - export const PRIMARY_PRODUCT_ID = getRandomProductId() -export const SECONDARY_PRODUCT_ID = getRandomProductId() \ No newline at end of file +export const SECONDARY_PRODUCT_ID = getRandomProductId() diff --git a/views/IDontNeedIt.tsx b/views/IDontNeedIt.tsx index 64877df..ee2df0e 100644 --- a/views/IDontNeedIt.tsx +++ b/views/IDontNeedIt.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import React from "react" import lightbulbIcon from "url:../assets/icons/Icons/Lightbulb.svg" import starIcon from "url:../assets/icons/Icons/Star.svg" import trophyIcon from "url:../assets/icons/Icons/Trophy.svg" diff --git a/views/ProductView.tsx b/views/ProductView.tsx index 486f418..f860c76 100644 --- a/views/ProductView.tsx +++ b/views/ProductView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import { useState } from "react" import clockIcon from "url:../assets/icons/Icons/Clock.svg" import lightbulbIcon from "url:../assets/icons/Icons/Lightbulb.svg" import thoughtfulIcon from "url:../assets/icons/Icons/Thoughtful.svg"