From 8e9bb0dc23e2f963673a25a494d58d549327a759 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 17 Aug 2026 21:23:35 +0300 Subject: [PATCH] =?UTF-8?q?test(landing):=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D1=83=20=D1=81=D0=BB=D0=B0=D0=B3=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=BF=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=81=D1=82=D0=B5=D0=B2=D0=BE=D0=B9=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BF=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Покрытия у слага не было вовсе. Пиним два инварианта: слаг попадает в тело запроса покупки и читается БЕЗ потребления — очистка на этом шаге лишила бы привязки того же клиента, если он позже войдёт в кабинет. Компоненты в репозитории не рендерятся (vitest на node, без jsdom), поэтому отправка фиксируется по исходнику; поведение самих хелперов проверяется обычными тестами с подменённым localStorage. --- src/utils/campaign.test.ts | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/utils/campaign.test.ts diff --git a/src/utils/campaign.test.ts b/src/utils/campaign.test.ts new file mode 100644 index 0000000..c4cfb35 --- /dev/null +++ b/src/utils/campaign.test.ts @@ -0,0 +1,91 @@ +import { readFileSync } from 'node:fs'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { consumeCampaignSlug, getPendingCampaignSlug } from './campaign'; + +const CAMPAIGN_KEY = 'campaign_slug'; +const CAMPAIGN_TTL_KEY = 'campaign_slug_ttl'; +const HOUR = 60 * 60 * 1000; + +function fakeLocalStorage(): Storage { + const store = new Map(); + return { + get length() { + return store.size; + }, + clear: () => store.clear(), + getItem: (key: string) => store.get(key) ?? null, + key: (index: number) => [...store.keys()][index] ?? null, + removeItem: (key: string) => void store.delete(key), + setItem: (key: string, value: string) => void store.set(key, value), + } as Storage; +} + +function storeSlug(slug: string, expiresInMs = 24 * HOUR): void { + localStorage.setItem(CAMPAIGN_KEY, slug); + localStorage.setItem(CAMPAIGN_TTL_KEY, String(Date.now() + expiresInMs)); +} + +beforeEach(() => { + vi.stubGlobal('localStorage', fakeLocalStorage()); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('getPendingCampaignSlug', () => { + it('returns the stored slug', () => { + storeSlug('summer_sale'); + + expect(getPendingCampaignSlug()).toBe('summer_sale'); + }); + + // Гостевая покупка на лендинге читает слаг этим геттером. Если он начнёт + // очищать хранилище, тот же клиент, зайдя потом в кабинет, потеряет + // привязку к кампании — auth-флоу уже ничего не найдёт. + it('does not consume the slug', () => { + storeSlug('summer_sale'); + + getPendingCampaignSlug(); + + expect(getPendingCampaignSlug()).toBe('summer_sale'); + expect(localStorage.getItem(CAMPAIGN_KEY)).toBe('summer_sale'); + }); + + it('drops an expired slug', () => { + storeSlug('summer_sale', -HOUR); + + expect(getPendingCampaignSlug()).toBeNull(); + expect(localStorage.getItem(CAMPAIGN_TTL_KEY)).toBeNull(); + }); + + it('returns null when nothing was captured', () => { + expect(getPendingCampaignSlug()).toBeNull(); + }); +}); + +describe('consumeCampaignSlug', () => { + it('clears the slug after reading it once', () => { + storeSlug('summer_sale'); + + expect(consumeCampaignSlug()).toBe('summer_sale'); + expect(consumeCampaignSlug()).toBeNull(); + }); +}); + +// Компоненты в этом репозитории не рендерятся в тестах (vitest на node, без +// jsdom), поэтому отправку слага фиксируем по исходнику: без неё покупка +// гостем не попадает в статистику кампании и не даёт её бонус. +describe('QuickPurchase source', () => { + const source = readFileSync(new URL('../pages/QuickPurchase.tsx', import.meta.url), 'utf8'); + + it('puts the campaign slug into the purchase payload', () => { + expect(source).toContain('data.campaign_slug = campaignSlug'); + }); + + it('reads the slug without consuming it', () => { + expect(source).toContain('getPendingCampaignSlug()'); + expect(source).not.toContain('consumeCampaignSlug'); + }); +});