diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..58d3e403 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Cloudhood is a cross-browser extension (Chrome & Firefox) for managing custom HTTP request headers. Users create profiles containing headers and URL filters, which are applied to web requests via the Declarative Net Request API. + +## Commands + +```bash +# Development +pnpm dev:chrome # Chrome dev with hot reload +pnpm dev:firefox # Firefox dev with file watch + +# Build +pnpm build # Build both browsers +pnpm build:chromium # Chrome only +pnpm build:firefox # Firefox only + +# Testing +pnpm test:unit # Unit tests (Vitest) +pnpm test:e2e # E2E tests interactive (Playwright) +pnpm test:e2e:ci # E2E tests in CI mode +pnpm test:e2e:screenshots # Visual regression tests +pnpm test:e2e:screenshots:docker # Visual regression in Docker (used in CI) + +# Linting +pnpm lint # ESLint +pnpm lint:css # Stylelint +``` + +Run a single unit test file: `pnpm test:unit src/shared/utils/__tests__/formatHeaders.test.ts` + +Run a single E2E test: `pnpm test:e2e tests/e2e/some-test.spec.ts` + +## Architecture + +### Feature-Sliced Design (FSD) + +The codebase follows FSD with strict layer imports — each layer can only import from layers below it: + +``` +app → pages → widgets → features → entities → shared +``` + +### Path Aliases + +TypeScript path aliases map to FSD layers: `#app`, `#pages/*`, `#widgets/*`, `#features/*`, `#entities/*`, `#shared/*`. Defined in `tsconfig.json`. + +### State Management — Effector + +All state is managed with **Effector** (stores, events, effects, `sample`). Each entity/feature has a `model/` directory containing Effector units. Data persists to `browser.storage` via effects. + +### Key Domain Model + +A **Profile** (`src/entities/request-profile/types.ts`) contains: +- `requestHeaders: RequestHeader[]` — name/value pairs applied to matching requests +- `urlFilters: UrlFilter[]` — URL patterns determining which requests get headers + +Headers are applied via Declarative Net Request in `src/shared/utils/setBrowserHeaders.ts`. + +### Browser Compatibility + +- `webextension-polyfill` wraps Chrome/Firefox API differences +- `src/shared/utils/browserAPI.ts` provides further abstraction (action API v2/v3 fallback) +- Separate manifests: `manifest.chromium.json`, `manifest.firefox.json` +- Build output goes to `build/chrome/` and `build/firefox/` + +### Build System + +Vite with two build targets: +1. **Popup** — React SPA (entry: `src/index.tsx`) +2. **Background** — Service worker (entry: `src/background.ts`, config: `vite.background.config.ts`) + +The `BROWSER` env var (`chrome` | `firefox`) controls which manifest and build output are used. + +### UI Components + +Uses `@snack-uikit/*` component library with `@emotion/styled` for CSS-in-JS styling. + +## Code Style + +- Prettier: 2-space indent, 120 print width, single quotes, trailing commas +- ESLint config from `@cloud-ru/eslint-config` with `eslint-plugin-effector` +- Commit messages validated by `@cloud-ru/ft-config-commit-message` (conventional commits) +- Pre-commit hooks via Husky + lint-staged + +## Tech Stack + +- React 18, TypeScript (strict), Vite, Effector +- pnpm (>=10), Node.js (>=20) +- Playwright (E2E), Vitest (unit), jsdom diff --git a/manifest.chromium.json b/manifest.chromium.json index 786764f2..7b3cb9ce 100644 --- a/manifest.chromium.json +++ b/manifest.chromium.json @@ -24,7 +24,8 @@ "storage", "declarativeNetRequest", "declarativeNetRequestFeedback", - "tabs" + "tabs", + "cookies" ], "background": { "service_worker": "background.bundle.js" diff --git a/manifest.dev.json b/manifest.dev.json index 0b4924c5..c654b9e5 100644 --- a/manifest.dev.json +++ b/manifest.dev.json @@ -22,7 +22,8 @@ "storage", "declarativeNetRequest", "declarativeNetRequestFeedback", - "tabs" + "tabs", + "cookies" ], "background": { "service_worker": "background.bundle.js" diff --git a/manifest.firefox.json b/manifest.firefox.json index 6f683b2f..120702ec 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -21,7 +21,8 @@ "declarativeNetRequest", "declarativeNetRequestFeedback", "activeTab", - "tabs" + "tabs", + "cookies" ], "host_permissions": [ "" diff --git a/scripts/firefox-e2e.mjs b/scripts/firefox-e2e.mjs index 454d2b04..45f0781b 100644 --- a/scripts/firefox-e2e.mjs +++ b/scripts/firefox-e2e.mjs @@ -30,6 +30,7 @@ const selectors = { profileSelect: '[data-test-id="profile-select"]', removeAllUrlFiltersButton: '[data-test-id="remove-all-url-filters-button"]', removeHeaderButton: '[data-test-id="remove-request-header-button"]', + removeProfileButton: '[data-test-id="remove-profile-button"]', removeUrlFilterButton: '[data-test-id="remove-url-filter-button"]', themeToggle: '[data-test-id="theme-toggle-button"]', urlFilterCheckbox: '[data-test-id="url-filter-checkbox"]', @@ -143,7 +144,12 @@ function createBrowser(driver, popupUrl) { await clickXpath(`//*[@role="menuitem" and contains(normalize-space(.), "${text}")]`, `menu item "${text}"`); }, clickTab: async text => { - await clickXpath(`//*[@role="tab" and contains(normalize-space(.), "${text}")]`, `tab "${text}"`); + const xpath = `//*[@role="tab" and contains(normalize-space(.), "${text}")]`; + await clickXpath(xpath, `tab "${text}"`); + await waitUntil(async () => { + const [tab] = await driver.findElements(By.xpath(xpath)); + return Boolean(tab) && (await tab.getAttribute('aria-selected')) === 'true'; + }, `tab "${text}" to become selected`); }, count: async selector => (await elements(selector)).length, dynamicRules: async () => driver.executeScript('return browser.declarativeNetRequest.getDynamicRules()'), @@ -251,7 +257,7 @@ async function main() { async () => { assert.equal(await browser.attr('[role="tab"]', 'aria-selected', 0), 'true'); await browser.clickTab('URL Filters'); - assert.equal(await browser.attr('[role="tab"]', 'aria-selected', 1), 'true'); + assert.equal(await browser.attr('[role="tab"]', 'aria-selected', 2), 'true'); assert.equal(await browser.count(selectors.urlFiltersSection), 1); await browser.clickTab('Headers'); assert.equal(await browser.attr('[role="tab"]', 'aria-selected', 0), 'true'); @@ -420,8 +426,11 @@ async function main() { async () => (await browser.count(selectors.profileNameInput)) === 0, 'profile rename to finish', ); - await browser.click(selectors.profileActionsButton); - await browser.clickMenuItem('Delete profile'); + await waitUntil( + () => browser.enabled(selectors.removeProfileButton), + 'remove profile button to become enabled', + ); + await browser.click(selectors.removeProfileButton); await waitForCount(browser, selectors.profileSelect, initialCount); assert.equal(await browser.value(selectors.headerNameInput), 'X-Profile-One'); }, diff --git a/scripts/firefox-screenshots.mjs b/scripts/firefox-screenshots.mjs index 8ff46386..8a246a7c 100644 --- a/scripts/firefox-screenshots.mjs +++ b/scripts/firefox-screenshots.mjs @@ -22,6 +22,14 @@ const selectors = { addHeaderButton: '[data-test-id="add-request-header-button"]', toggleAllHeadersButton: '[data-test-id="all-request-headers-checkbox"]', headerMenuButton: '[data-test-id="request-header-menu-button"]', + cookiesSection: '[data-test-id="profile-cookies-section"]', + cookieNameInput: '[data-test-id="cookie-name-input"] input', + cookieValueInput: '[data-test-id="cookie-value-input"] input', + cookieEnabledCheckbox: '[data-test-id="request-cookie-checkbox"]', + addCookieButton: '[data-test-id="add-request-cookie-button"]', + toggleAllCookiesButton: '[data-test-id="all-request-cookies-checkbox"]', + cookieMenuButton: '[data-test-id="request-cookie-menu-button"]', + removeAllCookiesButton: '[data-test-id="remove-all-request-cookies-button"]', urlFiltersSection: '[data-test-id="url-filters-section"]', urlFilterInput: '[data-test-id="url-filter-input"] input', urlFilterEnabledCheckbox: '[data-test-id="url-filter-checkbox"]', @@ -81,6 +89,44 @@ const scenarios = [ await addHeader('X-Menu', 'value'); await click(selectors.headerMenuButton); }), + scenario('cookies', 'empty-state', async ({ activateCookies }) => { + await activateCookies(); + }), + scenario('cookies', 'single-cookie', async ({ activateCookies, addCookie }) => { + await activateCookies(); + await addCookie('session_id', 'abc123'); + }), + scenario('cookies', 'multiple-cookies', async ({ activateCookies, addCookie }) => { + await activateCookies(); + await addCookie('session_id', 'abc123'); + await addCookie('theme', 'dark', 1); + }), + scenario('cookies', 'disabled-cookie', async ({ activateCookies, addCookie, click }) => { + await activateCookies(); + await addCookie('disabled_cookie', 'value'); + await click(selectors.cookieEnabledCheckbox); + }), + scenario('cookies', 'all-disabled', async ({ activateCookies, addCookie, click }) => { + await activateCookies(); + await addCookie('first_cookie', 'value-1'); + await addCookie('second_cookie', 'value-2', 1); + await click(selectors.toggleAllCookiesButton); + }), + scenario('cookies', 'name-validation-error', async ({ activateCookies, addCookie, blur }) => { + await activateCookies(); + await addCookie('invalid name=', 'value'); + await blur(selectors.cookieNameInput); + }), + scenario('cookies', 'value-validation-error', async ({ activateCookies, addCookie, blur }) => { + await activateCookies(); + await addCookie('session_id', 'invalid;value'); + await blur(selectors.cookieValueInput); + }), + scenario('cookies', 'cookie-menu-open', async ({ activateCookies, addCookie, click }) => { + await activateCookies(); + await addCookie('menu_cookie', 'value'); + await click(selectors.cookieMenuButton); + }), scenario('modals', 'export-modal', async ({ addHeader, click, clickMenuItem, waitText, blur }) => { await addHeader('X-Export', 'value'); await click(selectors.profileActionsButton); @@ -104,6 +150,12 @@ const scenarios = [ await removeAllUrlFilters(); await waitText('Remove all URL filters'); }), + scenario('modals', 'cookies-delete-confirmation', async ({ activateCookies, addCookie, removeAllCookies, waitText }) => { + await activateCookies(); + await addCookie('session_id', 'abc123'); + await removeAllCookies(); + await waitText('Remove all request cookies'); + }), scenario('profiles', 'single-profile', async ({ activateHeaders }) => { await activateHeaders(); }), @@ -238,10 +290,19 @@ function createBrowserHelpers(driver, popupUrl) { activateHeaders: async () => { await clickTab('Headers'); }, + activateCookies: async () => { + await clickTab('Request cookies'); + await waitUntil(() => visible(selectors.cookiesSection), 'cookies section'); + }, activateUrlFilters: async () => { await clickTab('URL Filters'); await waitUntil(() => visible(selectors.urlFiltersSection), 'URL filters section'); }, + addCookie: async (name, value, index = 0) => { + await click(selectors.addCookieButton); + await fill(selectors.cookieNameInput, name, index); + await fill(selectors.cookieValueInput, value, index); + }, addHeader: async (name, value, index = 0) => { await click(selectors.addHeaderButton); await fill(selectors.headerNameInput, name, index); @@ -251,6 +312,9 @@ function createBrowserHelpers(driver, popupUrl) { click, clickMenuItem, fill, + removeAllCookies: async () => { + await click(selectors.removeAllCookiesButton); + }, removeAllUrlFilters: async () => { if (await visible(selectors.removeAllUrlFiltersButton)) { await click(selectors.removeAllUrlFiltersButton); diff --git a/src/background.ts b/src/background.ts index bc4c1151..a999ab8b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -3,6 +3,7 @@ import browser from 'webextension-polyfill'; import { BrowserStorageKey, ServiceWorkerEvent } from './shared/constants'; import { browserAction } from './shared/utils/browserAPI'; import { logger, LogLevel } from './shared/utils/logger'; +import { setBrowserCookies } from './shared/utils/setBrowserCookies'; import { setBrowserHeaders } from './shared/utils/setBrowserHeaders'; import { enableExtensionReload } from './utils/extension-reload'; @@ -73,7 +74,7 @@ async function notify(message: ServiceWorkerEvent) { ]); logger.info('📦 Storage data for reload:', result); - await setBrowserHeaders(result, await getCurrentTabUrl()); + await Promise.all([setBrowserHeaders(result, await getCurrentTabUrl()), setBrowserCookies(result)]); } return undefined; } @@ -99,7 +100,7 @@ browser.runtime.onStartup.addListener(async function () { if (Object.keys(result).length) { logger.info('🚀 Storage data found, setting browser headers on startup'); try { - await setBrowserHeaders(result, await getCurrentTabUrl()); + await Promise.all([setBrowserHeaders(result, await getCurrentTabUrl()), setBrowserCookies(result)]); } catch (error) { logger.error('Failed to set browser headers on startup:', error); } @@ -127,7 +128,7 @@ browser.storage.onChanged.addListener(async (changes, areaName) => { ]); logger.debug('Storage changes data:', result); try { - await setBrowserHeaders(result, await getCurrentTabUrl()); + await Promise.all([setBrowserHeaders(result, await getCurrentTabUrl()), setBrowserCookies(result)]); } catch (error) { logger.error('Failed to set browser headers on storage change:', error); } @@ -157,7 +158,7 @@ browser.runtime.onInstalled.addListener(async details => { if (Object.keys(result).length) { logger.info('🔧 Storage data found, initializing browser headers on install/update'); try { - await setBrowserHeaders(result, await getCurrentTabUrl()); + await Promise.all([setBrowserHeaders(result, await getCurrentTabUrl()), setBrowserCookies(result)]); } catch (error) { logger.error('Failed to set browser headers on install/update:', error); } diff --git a/src/entities/profile-actions/model.ts b/src/entities/profile-actions/model.ts index 20d17918..f02dd8c0 100644 --- a/src/entities/profile-actions/model.ts +++ b/src/entities/profile-actions/model.ts @@ -2,7 +2,7 @@ import { createEvent, createStore } from 'effector'; import { selectedRequestProfileIdChanged } from '#entities/request-profile/model/selected-request-profile'; -export type ProfileActionsTab = 'headers' | 'url-filters'; +export type ProfileActionsTab = 'headers' | 'cookies' | 'url-filters'; export const profileActionsTabChanged = createEvent(); diff --git a/src/entities/request-profile/constants.ts b/src/entities/request-profile/constants.ts index c24afdb6..aae02cd2 100644 --- a/src/entities/request-profile/constants.ts +++ b/src/entities/request-profile/constants.ts @@ -13,6 +13,14 @@ export const DEFAULT_REQUEST_HEADERS: Profile[] = [ value: '', }, ], + requestCookies: [ + { + id: generateId(), + disabled: false, + name: '', + value: '', + }, + ], urlFilters: [ { id: generateId(), diff --git a/src/entities/request-profile/model/index.ts b/src/entities/request-profile/model/index.ts index 47fadaf5..c0d1a851 100644 --- a/src/entities/request-profile/model/index.ts +++ b/src/entities/request-profile/model/index.ts @@ -1,4 +1,5 @@ export * from './request-profiles'; export * from './selected-profile-url-filters'; +export * from './selected-request-cookies'; export * from './selected-request-headers'; export * from './selected-request-profile'; diff --git a/src/entities/request-profile/model/request-profiles.ts b/src/entities/request-profile/model/request-profiles.ts index 2bb6aab4..3180b56d 100644 --- a/src/entities/request-profile/model/request-profiles.ts +++ b/src/entities/request-profile/model/request-profiles.ts @@ -67,6 +67,7 @@ const profileAddedFx = attach({ { id: addedHeaderId, requestHeaders: [{ id: generateId(), name: '', value: '', disabled: false }], + requestCookies: [{ id: generateId(), name: '', value: '', disabled: false }], urlFilters: [{ id: generateId(), value: '', disabled: false }], }, ], diff --git a/src/entities/request-profile/model/selected-request-cookies.ts b/src/entities/request-profile/model/selected-request-cookies.ts new file mode 100644 index 00000000..12132adf --- /dev/null +++ b/src/entities/request-profile/model/selected-request-cookies.ts @@ -0,0 +1,19 @@ +import { combine } from 'effector'; + +import { validateCookie } from '#shared/utils/cookies'; + +import { $requestProfiles } from './request-profiles'; +import { $selectedRequestProfile } from './selected-request-profile'; + +export const $selectedProfileRequestCookies = combine( + $selectedRequestProfile, + $requestProfiles, + (selectedProfileId, profiles) => profiles.find(p => p.id === selectedProfileId)?.requestCookies ?? [], + { skipVoid: false }, +); + +export const $selectedProfileActiveRequestCookiesCount = combine( + $selectedProfileRequestCookies, + cookies => cookies.filter(c => !c.disabled && validateCookie(c.name, c.value)).length, + { skipVoid: false }, +); diff --git a/src/entities/request-profile/types.ts b/src/entities/request-profile/types.ts index 280f6ac2..d3548ca9 100644 --- a/src/entities/request-profile/types.ts +++ b/src/entities/request-profile/types.ts @@ -4,13 +4,27 @@ export type RequestHeader = { value: string; disabled: boolean; }; + +export type RequestCookie = { + id: number; + name: string; + value: string; + disabled: boolean; +}; + export type UrlFilter = { id: number; value: string; disabled: boolean; }; -export type Profile = { id: string; name?: string; requestHeaders: RequestHeader[]; urlFilters: UrlFilter[] }; +export type Profile = { + id: string; + name?: string; + requestHeaders: RequestHeader[]; + requestCookies: RequestCookie[]; + urlFilters: UrlFilter[]; +}; export type RemoveHeaderPayload = { headerId: number; diff --git a/src/features/export-profile/model.ts b/src/features/export-profile/model.ts index 57b4134c..e8f4edeb 100644 --- a/src/features/export-profile/model.ts +++ b/src/features/export-profile/model.ts @@ -8,7 +8,7 @@ import { copyToClipboard } from '#shared/utils/copyToClipboard'; import { COPY_RESULT_STATUS } from './constants'; import { downloadSelectedProfiles } from './utils'; -export const $profileExportList = createStore([]); +export const $profileExportList = combine($requestProfiles, profiles => profiles.map(p => p.id)); export const profileNameExportChanged = createEvent(); @@ -21,7 +21,13 @@ const $selectedProfileExportList = createStore([]).on(profileNameExpor export const $selectedExportProfileIdList = combine( $selectedProfileExportList, $profileExportList, - (selectedProfileName, profileIds) => profileIds.filter(profileId => selectedProfileName.includes(profileId)) || [], + $selectedRequestProfile, + (selectedProfileName, profileIds, currentProfileId) => { + // Until the user picks something explicitly, default to the currently selected profile. + const effectiveSelection = selectedProfileName.length > 0 ? selectedProfileName : [currentProfileId]; + + return profileIds.filter(profileId => effectiveSelection.includes(profileId)) || []; + }, { skipVoid: false }, ); @@ -40,11 +46,13 @@ export const $profileExportString = combine( profiles .filter(({ id }) => selectedExportProfileIdList.includes(id)) // eslint-disable-next-line @typescript-eslint/no-unused-vars -- if the model is extended, this may become an error - .map(({ id, requestHeaders, urlFilters, ...rest }) => ({ + .map(({ id, requestHeaders, requestCookies, urlFilters, ...rest }) => ({ ...rest, // eslint-disable-next-line @typescript-eslint/no-unused-vars -- if the model is extended, this may become an error requestHeaders: requestHeaders.map(({ id, ...headerRest }) => headerRest), // eslint-disable-next-line @typescript-eslint/no-unused-vars -- if the model is extended, this may become an error + requestCookies: (requestCookies ?? []).map(({ id, ...cookieRest }) => cookieRest), + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- if the model is extended, this may become an error urlFilters: urlFilters?.map(({ id, ...filterRest }) => filterRest) || [], })) || [], ); @@ -91,18 +99,6 @@ const profileExportDownloadFileFx = attach({ effect: serializedProfiles => downloadSelectedProfiles(serializedProfiles), }); -sample({ - source: $requestProfiles, - fn: profiles => profiles.map(p => p.id), - target: $profileExportList, -}); - -sample({ - source: $selectedRequestProfile, - fn: selectedProfile => [selectedProfile], - target: $selectedProfileExportList, -}); - sample({ source: profileExportStringChanged, target: $customProfileExportString, diff --git a/src/features/import-profile/extensions/model.ts b/src/features/import-profile/extensions/model.ts index 22937a36..ad8537c7 100644 --- a/src/features/import-profile/extensions/model.ts +++ b/src/features/import-profile/extensions/model.ts @@ -1,12 +1,15 @@ import { createEvent, createStore } from 'effector'; -import { Extensions } from '#shared/constants'; - export const profileImportExtensionNameChanged = createEvent(); export const profileImportExtensionNameCleared = createEvent(); export const profileImportExtensionNameReset = createEvent(); -export const $profileImportExtensionName = createStore(Extensions.ModHeader) +// Defaults to `null` so the regular "Import profile" flow never runs an extension adapter. +// The "Import from other extension" modal selects a concrete extension on mount (see +// ImportFromExtensionModal). This avoids a race: the wiring that clears the name on +// `importModalOpened` lives in a lazily-loaded chunk and may not be registered yet when the modal +// opens, so a ModHeader default leaked into plain imports and crashed the ModHeader adapter. +export const $profileImportExtensionName = createStore(null) .on(profileImportExtensionNameChanged, (_, string) => string) .on(profileImportExtensionNameCleared, () => null) .reset(profileImportExtensionNameReset); diff --git a/src/features/import-profile/utils/validateProfileList.ts b/src/features/import-profile/utils/validateProfileList.ts index 061c31cf..1e079e4a 100644 --- a/src/features/import-profile/utils/validateProfileList.ts +++ b/src/features/import-profile/utils/validateProfileList.ts @@ -1,4 +1,4 @@ -import { Profile, RequestHeader } from '#entities/request-profile/types'; +import { Profile, RequestCookie, RequestHeader } from '#entities/request-profile/types'; import { generateIdWithExcludeList } from '#shared/utils/generateId'; export function validateProfileList(profileList: Profile[], existingProfileList: Profile[]) { @@ -45,6 +45,32 @@ export function validateProfileList(profileList: Profile[], existingProfileList: } }); + if (profile.requestCookies !== undefined && !Array.isArray(profile.requestCookies)) { + throw new Error(`The profile ${currentProfileNumber} "requestCookies" value must be an array`); + } + + (profile.requestCookies ?? []).forEach((cookie: Partial, cookieIndex: number) => { + const currentCookieNumber = cookieIndex + 1; + + if (typeof cookie.name !== 'string') { + throw new Error( + `The cookie ${currentCookieNumber} in profile ${currentProfileNumber} must have a string "name" value`, + ); + } + + if (typeof cookie.value !== 'string') { + throw new Error( + `The cookie ${currentCookieNumber} in profile ${currentProfileNumber} must have a string "value" value`, + ); + } + + if (typeof cookie.disabled !== 'boolean') { + throw new Error( + `The cookie ${currentCookieNumber} in profile ${currentProfileNumber} must have a boolean "disabled" value`, + ); + } + }); + existingProfileList.forEach((existingProfile, existingProfileIndex) => { const currentExistingProfileNumber = existingProfileIndex + 1; @@ -74,6 +100,9 @@ export function generateProfileList(profileList: Profile[], existingProfileList: const existingProfileRequestHeadersListId = existingProfileList.flatMap(profile => profile.requestHeaders.map(header => header.id), ); + const existingProfileRequestCookiesListId = existingProfileList.flatMap(profile => + (profile.requestCookies ?? []).map(cookie => cookie.id), + ); return profileList.map(profile => ({ ...profile, @@ -82,6 +111,10 @@ export function generateProfileList(profileList: Profile[], existingProfileList: ...header, id: generateIdWithExcludeList(existingProfileRequestHeadersListId), })), + requestCookies: (profile.requestCookies ?? []).map((cookie: RequestCookie) => ({ + ...cookie, + id: generateIdWithExcludeList(existingProfileRequestCookiesListId), + })), urlFilters: profile.urlFilters || [], })); } diff --git a/src/features/selected-profile-request-cookies/add/model.ts b/src/features/selected-profile-request-cookies/add/model.ts new file mode 100644 index 00000000..cf5f7b7f --- /dev/null +++ b/src/features/selected-profile-request-cookies/add/model.ts @@ -0,0 +1,28 @@ +import { attach, createEvent, sample } from 'effector'; + +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; +import { RequestCookie } from '#entities/request-profile/types'; +import { generateId } from '#shared/utils/generateId'; + +type SelectedProfileRequestCookiesAdded = Omit[]; + +export const selectedProfileRequestCookiesAdded = createEvent(); + +const selectedProfileRequestCookiesAddedFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, requestCookies: SelectedProfileRequestCookiesAdded) => { + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + throw new Error('Profile not found'); + } + + return { + ...profile, + requestCookies: [...(profile.requestCookies ?? []), ...requestCookies.map(c => ({ ...c, id: generateId() }))], + }; + }, +}); + +sample({ clock: selectedProfileRequestCookiesAdded, target: selectedProfileRequestCookiesAddedFx }); +sample({ clock: selectedProfileRequestCookiesAddedFx.doneData, target: profileUpdated }); diff --git a/src/features/selected-profile-request-cookies/clear/model.ts b/src/features/selected-profile-request-cookies/clear/model.ts new file mode 100644 index 00000000..55c79faf --- /dev/null +++ b/src/features/selected-profile-request-cookies/clear/model.ts @@ -0,0 +1,27 @@ +import { attach, createEvent, sample } from 'effector'; + +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; +import { RequestCookie } from '#entities/request-profile/types'; + +export const selectedProfileRequestCookieCleared = createEvent(); + +const selectedProfileRequestCookiesClearedFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, requestCookieId: RequestCookie['id']) => { + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + throw new Error('Profile not found'); + } + + return { + ...profile, + requestCookies: (profile.requestCookies ?? []).map(requestCookie => + requestCookie.id === requestCookieId ? { ...requestCookie, value: '' } : requestCookie, + ), + }; + }, +}); + +sample({ clock: selectedProfileRequestCookieCleared, target: selectedProfileRequestCookiesClearedFx }); +sample({ clock: selectedProfileRequestCookiesClearedFx.doneData, target: profileUpdated }); diff --git a/src/features/selected-profile-request-cookies/copy/constants.ts b/src/features/selected-profile-request-cookies/copy/constants.ts new file mode 100644 index 00000000..72518729 --- /dev/null +++ b/src/features/selected-profile-request-cookies/copy/constants.ts @@ -0,0 +1,4 @@ +export enum COPY_RESULT_STATUS { + Success = 'Copied request cookie to clipboard.', + Error = 'Failed to copy request cookie.', +} diff --git a/src/features/selected-profile-request-cookies/copy/model.ts b/src/features/selected-profile-request-cookies/copy/model.ts new file mode 100644 index 00000000..4531ef46 --- /dev/null +++ b/src/features/selected-profile-request-cookies/copy/model.ts @@ -0,0 +1,36 @@ +import { createEffect, createEvent, sample } from 'effector'; + +import { notificationAdded } from '#entities/notification/model'; +import { RequestCookie } from '#entities/request-profile/types'; +import { copyToClipboard } from '#shared/utils/copyToClipboard'; + +import { COPY_RESULT_STATUS } from './constants'; + +type SelectedProfileRequestCookieCopied = Pick; + +export const selectedProfileRequestCookieCopied = createEvent(); + +const copySelectedProfileRequestCookieToClipboardFx = createEffect( + ({ name, value }: SelectedProfileRequestCookieCopied) => { + const isSupportClipboard = 'clipboard' in navigator; + if (!isSupportClipboard) { + throw new Error('Clipboard API is not supported in your browser'); + } + + copyToClipboard(`${name}=${value}`); + }, +); + +sample({ clock: selectedProfileRequestCookieCopied, target: copySelectedProfileRequestCookieToClipboardFx }); + +sample({ + source: copySelectedProfileRequestCookieToClipboardFx.doneData, + fn: () => ({ message: COPY_RESULT_STATUS.Success }), + target: notificationAdded, +}); + +sample({ + source: copySelectedProfileRequestCookieToClipboardFx.failData, + fn: () => ({ message: COPY_RESULT_STATUS.Error }), + target: notificationAdded, +}); diff --git a/src/features/selected-profile-request-cookies/duplicate/model.ts b/src/features/selected-profile-request-cookies/duplicate/model.ts new file mode 100644 index 00000000..85480950 --- /dev/null +++ b/src/features/selected-profile-request-cookies/duplicate/model.ts @@ -0,0 +1,32 @@ +import { attach, createEvent, sample } from 'effector'; + +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; +import { RequestCookie } from '#entities/request-profile/types'; +import { generateId } from '#shared/utils/generateId'; + +export const selectedProfileRequestCookieDuplicated = createEvent(); + +const selectedProfileRequestCookiesDuplicatedFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, requestCookieId: RequestCookie['id']) => { + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + throw new Error('Profile not found'); + } + + const currentRequestCookie = (profile.requestCookies ?? []).find(c => c.id === requestCookieId); + + if (!currentRequestCookie) { + throw new Error('Request cookie not found'); + } + + return { + ...profile, + requestCookies: [...(profile.requestCookies ?? []), { ...currentRequestCookie, id: generateId() }], + }; + }, +}); + +sample({ clock: selectedProfileRequestCookieDuplicated, target: selectedProfileRequestCookiesDuplicatedFx }); +sample({ clock: selectedProfileRequestCookiesDuplicatedFx.doneData, target: profileUpdated }); diff --git a/src/features/selected-profile-request-cookies/remove-all/constants.ts b/src/features/selected-profile-request-cookies/remove-all/constants.ts new file mode 100644 index 00000000..a503d25b --- /dev/null +++ b/src/features/selected-profile-request-cookies/remove-all/constants.ts @@ -0,0 +1,4 @@ +export enum REMOVE_ALL_REQUEST_COOKIES_RESULT_STATUS { + Success = 'All request cookies removed successfully.', + Error = 'Failed to remove request cookies.', +} diff --git a/src/features/selected-profile-request-cookies/remove-all/model.ts b/src/features/selected-profile-request-cookies/remove-all/model.ts new file mode 100644 index 00000000..ca53e8f7 --- /dev/null +++ b/src/features/selected-profile-request-cookies/remove-all/model.ts @@ -0,0 +1,39 @@ +import { attach, createEvent, sample } from 'effector'; + +import { notificationAdded } from '#entities/notification/model'; +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; + +import { REMOVE_ALL_REQUEST_COOKIES_RESULT_STATUS } from './constants'; + +export const selectedProfileAllRequestCookiesRemoved = createEvent(); + +const selectedProfileAllRequestCookiesRemovedFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }) => { + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + throw new Error('Profile not found'); + } + + return { + ...profile, + requestCookies: [], + }; + }, +}); + +sample({ clock: selectedProfileAllRequestCookiesRemoved, target: selectedProfileAllRequestCookiesRemovedFx }); +sample({ clock: selectedProfileAllRequestCookiesRemovedFx.doneData, target: profileUpdated }); + +sample({ + source: selectedProfileAllRequestCookiesRemovedFx.doneData, + fn: () => ({ message: REMOVE_ALL_REQUEST_COOKIES_RESULT_STATUS.Success }), + target: notificationAdded, +}); + +sample({ + source: selectedProfileAllRequestCookiesRemovedFx.failData, + fn: () => ({ message: REMOVE_ALL_REQUEST_COOKIES_RESULT_STATUS.Error }), + target: notificationAdded, +}); diff --git a/src/features/selected-profile-request-cookies/remove/model.ts b/src/features/selected-profile-request-cookies/remove/model.ts new file mode 100644 index 00000000..e5d93a82 --- /dev/null +++ b/src/features/selected-profile-request-cookies/remove/model.ts @@ -0,0 +1,25 @@ +import { attach, createEvent, sample } from 'effector'; + +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; +import { RequestCookie } from '#entities/request-profile/types'; + +export const selectedProfileRequestCookiesRemoved = createEvent(); + +const selectedProfileRequestCookiesRemovedFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, cookieIds: RequestCookie['id'][]) => { + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + throw new Error('Profile not found'); + } + + return { + ...profile, + requestCookies: (profile.requestCookies ?? []).filter(c => !cookieIds.includes(c.id)), + }; + }, +}); + +sample({ clock: selectedProfileRequestCookiesRemoved, target: selectedProfileRequestCookiesRemovedFx }); +sample({ clock: selectedProfileRequestCookiesRemovedFx.doneData, target: profileUpdated }); diff --git a/src/features/selected-profile-request-cookies/reorder/model.ts b/src/features/selected-profile-request-cookies/reorder/model.ts new file mode 100644 index 00000000..74fb8edd --- /dev/null +++ b/src/features/selected-profile-request-cookies/reorder/model.ts @@ -0,0 +1,97 @@ +import { DragOverEvent, DragStartEvent } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; +import { attach, combine, sample } from 'effector'; + +import { + $requestProfiles, + $selectedProfileRequestCookies, + $selectedRequestProfile, + profileUpdated, +} from '#entities/request-profile/model'; +import { + createSortableListModel, + dragEnded, + dragOver, + dragStarted, + type SortableItemId, + type SortableItemIdOrNull, +} from '#entities/sortable-list'; + +export const { + $flattenItems: $flattenRequestCookies, + $dragTarget: $dragTargetRequestCookies, + $raisedItem: $raisedRequestCookie, + reorderItems, + itemsUpdated, +} = createSortableListModel({ + $items: $selectedProfileRequestCookies, + $selectedItem: $selectedRequestProfile, + $allItems: $requestProfiles.map(profiles => profiles.map(profile => profile.requestCookies ?? [])), + itemsUpdated: profileUpdated, +}); + +export const $draggableRequestCookie = combine( + [$raisedRequestCookie, $selectedProfileRequestCookies], + ([raisedId, cookies]) => (raisedId ? cookies.find(cookie => cookie.id === raisedId) : null), +); + +const reorderRequestCookiesFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, payload: { active: string | number; target: string | number }) => { + const { active, target } = payload; + + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + return null; + } + + const requestCookies = profile.requestCookies ?? []; + const activeIndex = requestCookies.findIndex(cookie => cookie.id === active); + const targetIndex = requestCookies.findIndex(cookie => cookie.id === target); + + if (activeIndex === -1 || targetIndex === -1) { + return null; + } + + return { + ...profile, + requestCookies: arrayMove(requestCookies, activeIndex, targetIndex), + }; + }, +}); + +sample({ + clock: dragStarted, + filter: (event: DragStartEvent) => Boolean(event.active.id), + fn: (event: DragStartEvent) => event.active.id as string | number, + target: $raisedRequestCookie, +}); + +sample({ + clock: dragOver, + filter: (event: DragOverEvent) => Boolean(event.over?.id), + fn: (event: DragOverEvent) => event.over?.id as string | number, + target: $dragTargetRequestCookies, +}); + +const requestCookieMoved = sample({ + clock: dragEnded, + source: { active: $raisedRequestCookie, target: $dragTargetRequestCookies }, + filter(src: { + active: SortableItemIdOrNull; + target: SortableItemIdOrNull; + }): src is { active: SortableItemId; target: SortableItemId } { + return Boolean(src.active) && Boolean(src.target) && src.active !== src.target; + }, +}); + +sample({ clock: requestCookieMoved, target: reorderRequestCookiesFx }); +sample({ + clock: reorderRequestCookiesFx.doneData, + filter: Boolean, + target: profileUpdated, +}); + +$dragTargetRequestCookies.reset(reorderRequestCookiesFx.finally); +$raisedRequestCookie.reset(reorderRequestCookiesFx.finally); diff --git a/src/features/selected-profile-request-cookies/update/model.ts b/src/features/selected-profile-request-cookies/update/model.ts new file mode 100644 index 00000000..6f36ee7e --- /dev/null +++ b/src/features/selected-profile-request-cookies/update/model.ts @@ -0,0 +1,32 @@ +import { attach, createEvent, sample } from 'effector'; + +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; +import type { RequestCookie } from '#entities/request-profile/types'; + +export const selectedProfileRequestCookiesUpdated = createEvent(); + +const selectedProfileRequestCookiesUpdatedFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, updatedCookies: RequestCookie[]) => { + const profile = profiles.find(p => p.id === selectedProfile); + + if (!profile) { + throw new Error('Profile not found'); + } + + return { + ...profile, + requestCookies: (profile.requestCookies ?? []).map(cookie => { + const updatedCookie = updatedCookies.find(c => c.id === cookie.id); + if (updatedCookie) { + return updatedCookie; + } + + return cookie; + }), + }; + }, +}); + +sample({ clock: selectedProfileRequestCookiesUpdated, target: selectedProfileRequestCookiesUpdatedFx }); +sample({ clock: selectedProfileRequestCookiesUpdatedFx.doneData, target: profileUpdated }); diff --git a/src/features/selected-profile-request-headers/reorder/model.ts b/src/features/selected-profile-request-headers/reorder/model.ts index 0f60904b..f84c31eb 100644 --- a/src/features/selected-profile-request-headers/reorder/model.ts +++ b/src/features/selected-profile-request-headers/reorder/model.ts @@ -55,10 +55,8 @@ const reorderRequestHeadersFx = attach({ } return { - id: profile.id, - ...(profile.name && { name: profile.name }), + ...profile, requestHeaders: arrayMove(requestHeaders, activeIndex, targetIndex), - urlFilters: profile.urlFilters, }; }, }); diff --git a/src/features/selected-profile-url-filters/reorder/model.ts b/src/features/selected-profile-url-filters/reorder/model.ts index dc74e7aa..eb6ca9a1 100644 --- a/src/features/selected-profile-url-filters/reorder/model.ts +++ b/src/features/selected-profile-url-filters/reorder/model.ts @@ -54,9 +54,7 @@ const reorderUrlFiltersFx = attach({ } return { - id: profile.id, - ...(profile.name && { name: profile.name }), - requestHeaders: profile.requestHeaders, + ...profile, urlFilters: arrayMove(urlFilters, activeIndex, targetIndex), }; }, diff --git a/src/features/toggle-all-request-cookies/model.ts b/src/features/toggle-all-request-cookies/model.ts new file mode 100644 index 00000000..3a8fa308 --- /dev/null +++ b/src/features/toggle-all-request-cookies/model.ts @@ -0,0 +1,28 @@ +import { attach, combine, createEvent, sample } from 'effector'; + +import { $requestProfiles, $selectedRequestProfile, profileUpdated } from '#entities/request-profile/model'; +import { $selectedProfileRequestCookies } from '#entities/request-profile/model/selected-request-cookies'; + +export const toggleAllProfileRequestCookies = createEvent(); + +export const $isAllCookiesEnabled = combine( + $selectedProfileRequestCookies, + cookies => cookies.length > 0 && cookies.every(c => !c.disabled), + { skipVoid: false }, +); + +const toggleAllProfileRequestCookiesFx = attach({ + source: { profiles: $requestProfiles, selectedProfile: $selectedRequestProfile }, + effect: ({ profiles, selectedProfile }, enabled: boolean) => { + const profile = profiles.find(p => p.id === selectedProfile); + if (!profile) throw new Error('Profile not found'); + + return { + ...profile, + requestCookies: (profile.requestCookies ?? []).map(c => ({ ...c, disabled: !enabled })), + }; + }, +}); + +sample({ clock: toggleAllProfileRequestCookies, target: toggleAllProfileRequestCookiesFx }); +sample({ clock: toggleAllProfileRequestCookiesFx.doneData, target: profileUpdated }); diff --git a/src/pages/main/components/ProfileActions/CookiesActions/AllRequestCookiesCheckbox.tsx b/src/pages/main/components/ProfileActions/CookiesActions/AllRequestCookiesCheckbox.tsx new file mode 100644 index 00000000..bfd0cf7d --- /dev/null +++ b/src/pages/main/components/ProfileActions/CookiesActions/AllRequestCookiesCheckbox.tsx @@ -0,0 +1,19 @@ +import { useUnit } from 'effector-react'; + +import { Checkbox } from '@snack-uikit/toggles'; + +import { $isPaused } from '#entities/is-paused/model'; +import { $isAllCookiesEnabled, toggleAllProfileRequestCookies } from '#features/toggle-all-request-cookies/model'; + +export function AllRequestCookiesCheckbox() { + const { isAllEnabled, isPaused } = useUnit({ isAllEnabled: $isAllCookiesEnabled, isPaused: $isPaused }); + + return ( + + ); +} diff --git a/src/pages/main/components/ProfileActions/CookiesActions/CookiesActions.tsx b/src/pages/main/components/ProfileActions/CookiesActions/CookiesActions.tsx new file mode 100644 index 00000000..6a5aa9ea --- /dev/null +++ b/src/pages/main/components/ProfileActions/CookiesActions/CookiesActions.tsx @@ -0,0 +1,67 @@ +import { useUnit } from 'effector-react'; + +import { ButtonFunction } from '@snack-uikit/button'; +import { themeVars } from '@snack-uikit/figma-tokens'; +import { InfoFilledSVG, PlusSVG } from '@snack-uikit/icons'; +import { Tooltip } from '@snack-uikit/tooltip'; +import { Typography } from '@snack-uikit/typography'; + +import { $isPaused } from '#entities/is-paused/model'; +import { selectedProfileRequestCookiesAdded } from '#features/selected-profile-request-cookies/add/model'; +import { ProfileActionsLayout } from '#shared/components'; +import { RequestCookies } from '#widgets/request-cookies'; + +import { AllRequestCookiesCheckbox } from './AllRequestCookiesCheckbox'; +import { RemoveAllRequestCookies } from './RemoveAllRequestCookies'; +import * as S from './styled'; + +export function CookiesActions() { + const [isPaused, handleAddRequestCookie] = useUnit([$isPaused, selectedProfileRequestCookiesAdded]); + + const onAddRequestCookie = () => { + handleAddRequestCookie([{ disabled: false, name: '', value: '' }]); + }; + + const leftHeaderActions = ( + <> + + Request cookies + + Cookies are added to existing browser cookies for matching URLs + ⚠️ At least one URL filter is required for cookies to work + example.com - exact domain match + https://api.example.com/* - all API requests via HTTPS + *://example.com/* - requests via any protocol + *://domain*/* - matches subdomains like domain-dev, domain.cloud + * means "any value", /* - all paths + ⚠️ *://domain/* and *://domain/ won't match subdomains + + } + placement='top' + trigger='click' + > + + + + ); + + const rightHeaderActions = ( + <> + } + onClick={onAddRequestCookie} + /> + + + ); + + return ( + + + + ); +} diff --git a/src/pages/main/components/ProfileActions/CookiesActions/RemoveAllRequestCookies.tsx b/src/pages/main/components/ProfileActions/CookiesActions/RemoveAllRequestCookies.tsx new file mode 100644 index 00000000..c54537c6 --- /dev/null +++ b/src/pages/main/components/ProfileActions/CookiesActions/RemoveAllRequestCookies.tsx @@ -0,0 +1,52 @@ +import { useUnit } from 'effector-react'; +import { useState } from 'react'; + +import { ButtonFunction } from '@snack-uikit/button'; +import { TrashSVG } from '@snack-uikit/icons'; +import { Modal } from '@snack-uikit/modal'; +import { Typography } from '@snack-uikit/typography'; + +import { $isPaused } from '#entities/is-paused/model'; +import { $selectedProfileRequestCookies } from '#entities/request-profile/model'; +import { selectedProfileAllRequestCookiesRemoved } from '#features/selected-profile-request-cookies/remove-all/model'; + +export function RemoveAllRequestCookies() { + const [isOpen, setIsOpen] = useState(false); + const [isPaused, handleRemoveAllRequestCookies, requestCookies] = useUnit([ + $isPaused, + selectedProfileAllRequestCookiesRemoved, + $selectedProfileRequestCookies, + ]); + + const handleClose = () => { + setIsOpen(false); + }; + + const handleRemove = () => { + handleRemoveAllRequestCookies(); + setIsOpen(false); + }; + + return ( + <> + } + disabled={isPaused || requestCookies.length === 0} + onClick={() => setIsOpen(true)} + data-test-id='remove-all-request-cookies-button' + /> + + All request cookies will be removed from the list. Do you really want to remove cookies? + + } + approveButton={{ onClick: handleRemove, label: 'Delete', appearance: 'destructive' }} + cancelButton={{ onClick: handleClose, label: 'Cancel' }} + /> + + ); +} diff --git a/src/pages/main/components/ProfileActions/CookiesActions/index.ts b/src/pages/main/components/ProfileActions/CookiesActions/index.ts new file mode 100644 index 00000000..8708a268 --- /dev/null +++ b/src/pages/main/components/ProfileActions/CookiesActions/index.ts @@ -0,0 +1 @@ +export { CookiesActions } from './CookiesActions'; diff --git a/src/pages/main/components/ProfileActions/CookiesActions/styled.ts b/src/pages/main/components/ProfileActions/CookiesActions/styled.ts new file mode 100644 index 00000000..5b306754 --- /dev/null +++ b/src/pages/main/components/ProfileActions/CookiesActions/styled.ts @@ -0,0 +1,9 @@ +import styled from '@emotion/styled'; + +import { themeVars } from '@snack-uikit/figma-tokens'; + +export const Ul = styled.ul``; + +export const Li = styled.li` + ${themeVars.sans.body.m}; +`; diff --git a/src/pages/main/components/ProfileActions/ProfileActions.tsx b/src/pages/main/components/ProfileActions/ProfileActions.tsx index a684adab..80ffafb1 100644 --- a/src/pages/main/components/ProfileActions/ProfileActions.tsx +++ b/src/pages/main/components/ProfileActions/ProfileActions.tsx @@ -4,39 +4,41 @@ import { Tabs } from '@snack-uikit/tabs'; import { $isPaused } from '#entities/is-paused/model'; import { $activeProfileActionsTab, profileActionsTabChanged } from '#entities/profile-actions'; -import { $selectedProfileActiveRequestHeadersCount, $selectedProfileActiveUrlFiltersCount } from '#entities/request-profile/model'; +import { + $selectedProfileActiveRequestCookiesCount, + $selectedProfileActiveRequestHeadersCount, + $selectedProfileActiveUrlFiltersCount, +} from '#entities/request-profile/model'; import { getCounterProps } from '#shared/utils/getCounterProps'; +import { CookiesActions } from './CookiesActions'; import { RequestHeadersActions } from './RequestHeadersActions'; import * as S from './styled'; import { UrlFiltersActions } from './UrlFiltersActions'; export function ProfileActions() { - const [isPaused, activeTab, activeRequestHeadersCount, activeUrlFiltersCount] = useUnit([ + const [isPaused, activeTab, activeRequestHeadersCount, activeRequestCookiesCount, activeUrlFiltersCount] = useUnit([ $isPaused, $activeProfileActionsTab, $selectedProfileActiveRequestHeadersCount, - $selectedProfileActiveUrlFiltersCount + $selectedProfileActiveRequestCookiesCount, + $selectedProfileActiveUrlFiltersCount, ]); return ( - - + + + + + + diff --git a/src/pages/main/components/ProfileActions/RequestHeadersActions/RequestHeadersActions.tsx b/src/pages/main/components/ProfileActions/RequestHeadersActions/RequestHeadersActions.tsx index e411b15b..e6787aea 100644 --- a/src/pages/main/components/ProfileActions/RequestHeadersActions/RequestHeadersActions.tsx +++ b/src/pages/main/components/ProfileActions/RequestHeadersActions/RequestHeadersActions.tsx @@ -40,6 +40,7 @@ export function RequestHeadersActions() { icon={} onClick={onAddRequestHeader} /> + } diff --git a/src/shared/utils/cookies.ts b/src/shared/utils/cookies.ts new file mode 100644 index 00000000..db50b03c --- /dev/null +++ b/src/shared/utils/cookies.ts @@ -0,0 +1,11 @@ +// RFC 6265 cookie-name: token characters only (no separators) +export const validateCookieName = (name: string): boolean => + typeof name === 'string' && name.length > 0 && /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/.test(name); + +// RFC 6265 cookie-value: no semicolons, no control chars +export const validateCookieValue = (value: string): boolean => + // eslint-disable-next-line no-control-regex + typeof value === 'string' && value.length > 0 && !/[;\x00-\x1f\x7f]/.test(value); + +export const validateCookie = (name: string, value: string): boolean => + validateCookieName(name) && validateCookieValue(value); diff --git a/src/shared/utils/setBrowserCookies.ts b/src/shared/utils/setBrowserCookies.ts new file mode 100644 index 00000000..8faf7778 --- /dev/null +++ b/src/shared/utils/setBrowserCookies.ts @@ -0,0 +1,114 @@ +import browser from 'webextension-polyfill'; + +import type { Profile } from '#entities/request-profile/types'; +import { BrowserStorageKey } from '#shared/constants'; + +import { validateCookie } from './cookies'; +import { logger } from './logger'; + +// Track cookies managed by the extension so we can clean them up +// Key format: `${url}::${name}` +const managedCookies = new Set(); + +/** + * Converts a URL filter pattern to a list of concrete URLs suitable for browser.cookies.set(). + * URL filters can be: full URLs, domains with wildcards, or regexFilter patterns. + * For cookies, we need a concrete URL with protocol. + */ +function urlFilterToBaseUrls(urlFilter: string): string[] { + // If it already looks like a full URL, use it directly + if (urlFilter.startsWith('http://') || urlFilter.startsWith('https://')) { + // Strip wildcards from the path for cookies API + const url = urlFilter.replace(/\*/g, ''); + return [url]; + } + + // If it's a domain-like pattern (e.g., "*.example.com" or "example.com") + const domain = urlFilter.replace(/^\*\.?/, '').replace(/\/.*$/, ''); + if (domain) { + return [`https://${domain}/`]; + } + + return []; +} + +export async function setBrowserCookies(result: Record) { + const isPaused = result[BrowserStorageKey.IsPaused] as boolean; + + let profiles: Profile[] = []; + let selectedProfile = ''; + + try { + const profilesData = result[BrowserStorageKey.Profiles]; + if (profilesData && typeof profilesData === 'string') { + profiles = JSON.parse(profilesData); + } else if (Array.isArray(profilesData)) { + profiles = profilesData as Profile[]; + } + } catch (error) { + logger.error('Failed to parse profiles from storage (setBrowserCookies):', error); + profiles = []; + } + + const selectedProfileData = result[BrowserStorageKey.SelectedProfile]; + if (selectedProfileData && typeof selectedProfileData === 'string') { + selectedProfile = selectedProfileData; + } + + const profile = profiles.find(p => p.id === selectedProfile); + + // Remove all previously managed cookies + await Promise.allSettled( + Array.from(managedCookies).map(key => { + const separatorIndex = key.indexOf('::'); + const url = key.substring(0, separatorIndex); + const name = key.substring(separatorIndex + 2); + return browser.cookies.remove({ url, name }).then( + () => logger.debug(`Removed managed cookie: ${name} for ${url}`), + err => logger.warn(`Failed to remove cookie ${name} for ${url}:`, err), + ); + }), + ); + managedCookies.clear(); + + if (isPaused) { + return; + } + + const selectedProfileCookies = profile?.requestCookies ?? []; + const selectedProfileUrlFilters = profile?.urlFilters ?? []; + + const activeCookies = selectedProfileCookies.filter( + ({ disabled, name, value }) => !disabled && validateCookie(name, value), + ); + + if (activeCookies.length === 0) { + return; + } + + const activeUrlFilters = selectedProfileUrlFilters + .filter(({ disabled, value }) => !disabled && value.trim()) + .map(({ value }) => value.trim()); + + // Collect target URLs from URL filters + const targetUrls = activeUrlFilters.flatMap(urlFilterToBaseUrls); + + if (targetUrls.length === 0) { + logger.warn('No valid URL filters found for cookies — cookies require at least one URL filter to set the domain'); + return; + } + + await Promise.allSettled( + targetUrls.flatMap(url => + activeCookies.map(cookie => { + managedCookies.add(`${url}::${cookie.name}`); + return browser.cookies.set({ url, name: cookie.name, value: cookie.value }).then( + () => logger.debug(`Set cookie: ${cookie.name}=${cookie.value} for ${url}`), + err => logger.warn(`Failed to set cookie ${cookie.name} for ${url}:`, err), + ); + }), + ), + ); + + logger.info(`🍪 Cookies updated: ${managedCookies.size} cookies set across ${targetUrls.length} URLs`); +} diff --git a/src/shared/utils/setBrowserHeaders.ts b/src/shared/utils/setBrowserHeaders.ts index 3185789d..463a5930 100644 --- a/src/shared/utils/setBrowserHeaders.ts +++ b/src/shared/utils/setBrowserHeaders.ts @@ -3,29 +3,30 @@ import browser from 'webextension-polyfill'; import type { Profile, RequestHeader } from '#entities/request-profile/types'; import { BrowserStorageKey } from '#shared/constants'; -import { countActiveHeadersForUrl } from './countActiveHeadersForUrl'; +import { validateCookie } from './cookies'; +import { countActiveHeadersForUrl, doesUrlMatchFilter } from './countActiveHeadersForUrl'; import { createUrlCondition } from './createUrlCondition'; import { validateHeader } from './headers'; import { logger } from './logger'; import { setIconBadge } from './setIconBadge'; -function getRulesForHeader(header: RequestHeader, urlFilters: string[]): browser.DeclarativeNetRequest.Rule[] { - const allResourceTypes = [ - 'main_frame', - 'sub_frame', - 'stylesheet', - 'script', - 'image', - 'font', - 'object', - 'xmlhttprequest', - 'ping', - 'csp_report', - 'media', - 'websocket', - 'other', - ] as browser.DeclarativeNetRequest.ResourceType[]; +const allResourceTypes = [ + 'main_frame', + 'sub_frame', + 'stylesheet', + 'script', + 'image', + 'font', + 'object', + 'xmlhttprequest', + 'ping', + 'csp_report', + 'media', + 'websocket', + 'other', +] as browser.DeclarativeNetRequest.ResourceType[]; +function getRulesForHeader(header: RequestHeader, urlFilters: string[]): browser.DeclarativeNetRequest.Rule[] { // If there are no URL filters, apply the header to all URLs if (urlFilters.length === 0) { return [ @@ -129,10 +130,40 @@ export async function setBrowserHeaders(result: Record, current activeUrlFiltersCount: activeUrlFilters.length, }); - const addRules: browser.DeclarativeNetRequest.Rule[] = !isPaused + const headerRules: browser.DeclarativeNetRequest.Rule[] = !isPaused ? activeHeaders.flatMap(header => getRulesForHeader(header, activeUrlFilters)) : []; + // Build cookie rules as fallback when browser.cookies API can't be used (no URL filters) + // When URL filters exist, cookies are handled by setBrowserCookies via browser.cookies.set() + const selectedProfileCookies = profile?.requestCookies ?? []; + const activeCookies = selectedProfileCookies.filter( + ({ disabled, name, value }) => !disabled && validateCookie(name, value), + ); + + const cookieRules: browser.DeclarativeNetRequest.Rule[] = []; + if (!isPaused && activeCookies.length > 0 && activeUrlFilters.length === 0) { + const cookieHeaderValue = activeCookies.map(c => `${c.name}=${c.value}`).join('; '); + const cookieRuleBaseId = 900000; + + cookieRules.push({ + id: cookieRuleBaseId, + action: { + type: 'modifyHeaders' as const, + // `append` merges our cookies with the request's existing Cookie header instead of + // replacing it (`set`). The browser inserts the correct `; ` separator automatically. + // NOTE: the append allowlist is case-sensitive — the header name MUST be lowercase + // `cookie` (Chrome bug 449152902), otherwise the rule is silently rejected. + requestHeaders: [{ header: 'cookie', value: cookieHeaderValue, operation: 'append' as const }], + }, + condition: { + resourceTypes: allResourceTypes, + }, + }); + } + + const addRules = [...headerRules, ...cookieRules]; + const removeRuleIds = currentRules.map(item => item.id); try { @@ -140,20 +171,12 @@ export async function setBrowserHeaders(result: Record, current logger.info('Remove rule IDs:', removeRuleIds); logger.info('Add rules:', addRules); - if (removeRuleIds.length > 0) { + if (removeRuleIds.length > 0 || addRules.length > 0) { await browser.declarativeNetRequest.updateDynamicRules({ removeRuleIds, - addRules: [], - }); - logger.debug('Old rules removed'); - } - - if (addRules.length > 0) { - await browser.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: [], addRules, }); - logger.debug('New rules added'); + logger.debug('Rules updated'); } const updatedRules = await browser.declarativeNetRequest.getDynamicRules(); @@ -162,7 +185,12 @@ export async function setBrowserHeaders(result: Record, current logger.info('Rules updated successfully'); logger.groupEnd(); - const activeRulesCount = countActiveHeadersForUrl(activeHeaders, activeUrlFilters, currentTabUrl); + const tabUrl = currentTabUrl; + const urlMatchesCurrentTab = + activeUrlFilters.length === 0 || !tabUrl || activeUrlFilters.some(filter => doesUrlMatchFilter(tabUrl, filter)); + const activeCookiesCount = urlMatchesCurrentTab ? activeCookies.length : 0; + const activeRulesCount = + countActiveHeadersForUrl(activeHeaders, activeUrlFilters, currentTabUrl) + activeCookiesCount; await setIconBadge({ isPaused, activeRulesCount }); } catch (err) { logger.error('Failed to update dynamic rules:', err); diff --git a/src/widgets/header/Header.tsx b/src/widgets/header/Header.tsx index 551d7443..470f0a12 100644 --- a/src/widgets/header/Header.tsx +++ b/src/widgets/header/Header.tsx @@ -2,9 +2,10 @@ import { useUnit } from 'effector-react'; import { useCallback, useState } from 'react'; import { ButtonFunction } from '@snack-uikit/button'; -import { KebabSVG } from '@snack-uikit/icons'; +import { KebabSVG, TrashSVG } from '@snack-uikit/icons'; -import { $selectedProfileIndex } from '#entities/request-profile/model'; +import { $isProfileRemoveAvailable, $selectedProfileIndex } from '#entities/request-profile/model'; +import { selectedProfileRemoved } from '#features/selected-profile/remove/model'; import { useActions } from '#widgets/header/hooks'; import { CopyActiveRequestHeaders } from './components/CopyActiveRequestHeaders'; @@ -13,7 +14,11 @@ import { ProfileNameField } from './components/ProfileNameField'; import * as S from './styled'; export function Header() { - const [selectedProfileIndex] = useUnit([$selectedProfileIndex]); + const [selectedProfileIndex, isProfileRemoveAvailable, handleRemove] = useUnit([ + $selectedProfileIndex, + $isProfileRemoveAvailable, + selectedProfileRemoved, + ]); const [isOpen, setIsOpen] = useState(false); const onClose = useCallback(() => { @@ -30,6 +35,15 @@ export function Header() { + } + disabled={!isProfileRemoveAvailable} + onClick={handleRemove} + data-test-id='remove-profile-button' + /> + diff --git a/src/widgets/header/hooks.tsx b/src/widgets/header/hooks.tsx index 4408a50c..df4c4b88 100644 --- a/src/widgets/header/hooks.tsx +++ b/src/widgets/header/hooks.tsx @@ -1,12 +1,11 @@ import { useUnit } from 'effector-react'; import { useCallback, useMemo } from 'react'; -import { DownloadSVG, PlusSVG, TrashSVG, UploadSVG } from '@snack-uikit/icons'; +import { DownloadSVG, PlusSVG, UploadSVG } from '@snack-uikit/icons'; import { exportModalOpened, importFromExtensionModalOpened, importModalOpened } from '#entities/modal/model'; import { $activeProfileActionsTab, profileActionsTabChanged } from '#entities/profile-actions'; -import { $isProfileRemoveAvailable, profileAdded } from '#entities/request-profile/model'; -import { selectedProfileRemoved } from '#features/selected-profile/remove/model'; +import { profileAdded } from '#entities/request-profile/model'; import { profileUrlFiltersAdded } from '#features/selected-profile-url-filters/add/model'; import { FileOpenSVG, FileUploadSVG } from '#shared/assets/svg'; @@ -16,22 +15,19 @@ type UseActionsProps = { export function useActions({ onClose }: UseActionsProps) { const [ - isProfileRemoveAvailable, activeTab, onProfileAdded, onImportModalOpened, onImportFromExtensionModalOpened, - onSelectedProfileRemoved, + onExportModalOpened, onProfileUrlFiltersAdded, onProfileActionsTabChanged, ] = useUnit([ - $isProfileRemoveAvailable, $activeProfileActionsTab, profileAdded, importModalOpened, importFromExtensionModalOpened, - selectedProfileRemoved, exportModalOpened, profileUrlFiltersAdded, profileActionsTabChanged, @@ -52,11 +48,6 @@ export function useActions({ onClose }: UseActionsProps) { onClose(); }, [onClose, onImportFromExtensionModalOpened]); - const handleRemoveProfile = useCallback(() => { - onSelectedProfileRemoved(); - onClose(); - }, [onClose, onSelectedProfileRemoved]); - const handleExportModalOpened = useCallback(() => { onExportModalOpened(); onClose(); @@ -102,21 +93,12 @@ export function useActions({ onClose }: UseActionsProps) { beforeContent: , onClick: handleExportModalOpened, }, - { - id: 'remove', - content: { option: 'Delete profile' }, - beforeContent: , - onClick: handleRemoveProfile, - disabled: !isProfileRemoveAvailable, - }, ], [ handleAddProfile, handleExportModalOpened, handleOpenImportFromExtensionModal, handleOpenImportModal, - handleRemoveProfile, - isProfileRemoveAvailable, handleAddUrlFilter, ], ); diff --git a/src/widgets/modals/Modals.tsx b/src/widgets/modals/Modals.tsx deleted file mode 100644 index c5e4b784..00000000 --- a/src/widgets/modals/Modals.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useUnit } from 'effector-react'; - -import { $isExportModalOpen, $isimportFromExtensionModalOpen, $isImportModalOpen } from '#entities/modal/model'; - -import { ExportModal } from './components/ExportModal'; -import { ImportFromExtensionModal } from './components/ImportFromExtensionModal'; -import { ImportModal } from './components/ImportModal'; - -export function Modals() { - const [isImportModalOpen, isImportFromExtensionModalOpen, isExportModalOpen] = useUnit([ - $isImportModalOpen, - $isimportFromExtensionModalOpen, - $isExportModalOpen, - ]); - - return ( - <> - {isImportModalOpen && } - {isImportFromExtensionModalOpen && } - {isExportModalOpen && } - - ); -} diff --git a/src/widgets/modals/components/ImportFromExtensionModal/ImportFromExtensionModal.tsx b/src/widgets/modals/components/ImportFromExtensionModal/ImportFromExtensionModal.tsx index 78cdecb8..8a69601d 100644 --- a/src/widgets/modals/components/ImportFromExtensionModal/ImportFromExtensionModal.tsx +++ b/src/widgets/modals/components/ImportFromExtensionModal/ImportFromExtensionModal.tsx @@ -46,6 +46,16 @@ export function ImportFromExtensionModal() { const textFieldRef = useRef(null); + // Preselect ModHeader when opening the modal without a chosen extension. Done here (not via a + // store default) so the plain "Import profile" flow keeps a `null` extension name and never runs + // an adapter. + useEffect(() => { + if (!profileImportExtensionName) { + onProfileImportExtensionNameChanged(Extensions.ModHeader); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { if (isError && textFieldRef.current) { textFieldRef.current.focus(); diff --git a/src/widgets/modals/index.ts b/src/widgets/modals/index.ts index c363a444..f6bc5e3d 100644 --- a/src/widgets/modals/index.ts +++ b/src/widgets/modals/index.ts @@ -1,2 +1 @@ export * from './LazyModals'; -export * from './Modals'; diff --git a/src/widgets/request-cookies/RequestCookies.tsx b/src/widgets/request-cookies/RequestCookies.tsx new file mode 100644 index 00000000..7a60e542 --- /dev/null +++ b/src/widgets/request-cookies/RequestCookies.tsx @@ -0,0 +1,47 @@ +import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext } from '@dnd-kit/sortable'; +import { useUnit } from 'effector-react'; + +import { $selectedProfileRequestCookies } from '#entities/request-profile/model/selected-request-cookies'; +import { dragEnded, dragOver, dragStarted, restrictToParentElement } from '#entities/sortable-list'; +import { + $draggableRequestCookie, + $flattenRequestCookies, +} from '#features/selected-profile-request-cookies/reorder/model'; +import { isDefined } from '#shared/utils/typeGuards'; + +import { RequestCookieRow } from './components/RequestCookieRow/RequestCookieRow'; +import * as S from './styled'; + +export function RequestCookies() { + const { requestCookies, flattenRequestCookies, activeRequestCookie, onDragStarted, onDragOver, onDragEnded } = + useUnit({ + requestCookies: $selectedProfileRequestCookies, + flattenRequestCookies: $flattenRequestCookies, + activeRequestCookie: $draggableRequestCookie, + onDragStarted: dragStarted, + onDragOver: dragOver, + onDragEnded: dragEnded, + }); + + const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor)); + + return ( + + + + {requestCookies.map(cookie => ( + + ))} + + + {isDefined(activeRequestCookie) ? : null} + + ); +} diff --git a/src/widgets/request-cookies/components/RequestCookieRow/RequestCookieMenu.tsx b/src/widgets/request-cookies/components/RequestCookieRow/RequestCookieMenu.tsx new file mode 100644 index 00000000..24bae385 --- /dev/null +++ b/src/widgets/request-cookies/components/RequestCookieRow/RequestCookieMenu.tsx @@ -0,0 +1,61 @@ +import { useUnit } from 'effector-react'; +import { useState } from 'react'; + +import { ButtonFunction } from '@snack-uikit/button'; +import { CopySVG, CrossSVG, KebabSVG } from '@snack-uikit/icons'; + +import { $isPaused } from '#entities/is-paused/model'; +import { RequestCookie } from '#entities/request-profile/types'; +import { selectedProfileRequestCookieCleared } from '#features/selected-profile-request-cookies/clear/model'; +import { selectedProfileRequestCookieCopied } from '#features/selected-profile-request-cookies/copy/model'; +import { selectedProfileRequestCookieDuplicated } from '#features/selected-profile-request-cookies/duplicate/model'; +import { DuplicateSVG } from '#shared/assets/svg'; + +import * as S from './styled'; + +export function RequestCookieMenu({ id, name, value }: RequestCookie) { + const [handleDuplicate, handleRequestCookieCopy, handleClear, isPaused] = useUnit([ + selectedProfileRequestCookieDuplicated, + selectedProfileRequestCookieCopied, + selectedProfileRequestCookieCleared, + $isPaused, + ]); + + const [isOpen, setIsOpen] = useState(false); + + const handleCopy = () => { + handleRequestCookieCopy({ name, value }); + setIsOpen(false); + }; + + return ( + , + onClick: () => handleDuplicate(id), + }, + { + id: 'copy-value', + content: { option: 'Copy' }, + beforeContent: , + onClick: handleCopy, + }, + { + id: 'clear-value', + content: { option: 'Clear Value' }, + beforeContent: , + onClick: () => handleClear(id), + }, + ]} + > + } disabled={isPaused} /> + + ); +} diff --git a/src/widgets/request-cookies/components/RequestCookieRow/RequestCookieRow.tsx b/src/widgets/request-cookies/components/RequestCookieRow/RequestCookieRow.tsx new file mode 100644 index 00000000..3e545cd7 --- /dev/null +++ b/src/widgets/request-cookies/components/RequestCookieRow/RequestCookieRow.tsx @@ -0,0 +1,117 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { useUnit } from 'effector-react/effector-react.mjs'; +import { type KeyboardEvent, useState } from 'react'; + +import { ButtonFunction } from '@snack-uikit/button'; +import { CrossSVG } from '@snack-uikit/icons'; +import { Checkbox, CheckboxProps } from '@snack-uikit/toggles'; +import { Tooltip } from '@snack-uikit/tooltip'; + +import { $isPaused } from '#entities/is-paused/model'; +import type { RequestCookie } from '#entities/request-profile/types'; +import { DragHandle } from '#entities/sortable-list'; +import { selectedProfileRequestCookiesRemoved } from '#features/selected-profile-request-cookies/remove/model'; +import { selectedProfileRequestCookiesUpdated } from '#features/selected-profile-request-cookies/update/model'; +import { validateCookieName, validateCookieValue } from '#shared/utils/cookies'; + +import { RequestCookieMenu } from './RequestCookieMenu'; +import * as S from './styled'; + +export function RequestCookieRow(props: RequestCookie) { + const { disabled, name, value, id } = props; + const { setNodeRef, listeners, attributes, transition, transform, isDragging } = useSortable({ id }); + const { isPaused, onRequestCookiesUpdated, onRequestCookiesRemoved } = useUnit({ + isPaused: $isPaused, + onRequestCookiesUpdated: selectedProfileRequestCookiesUpdated, + onRequestCookiesRemoved: selectedProfileRequestCookiesRemoved, + }); + + const isNameFormatVerified = validateCookieName(name); + const isValueFormatVerified = validateCookieValue(value); + + const [cookieNameFocused, setCookieNameFocused] = useState(false); + + const handleKeyPress = (event: KeyboardEvent) => { + const target = event.target as HTMLInputElement; + if (event.key === ' ' && target.selectionStart === 0) { + event.preventDefault(); + } + }; + + const handleChange = (field: 'value' | 'name') => (val: string) => { + onRequestCookiesUpdated([{ ...props, [field]: val }]); + }; + + const handleChecked: CheckboxProps['onChange'] = checked => { + onRequestCookiesUpdated([{ ...props, disabled: !checked }]); + }; + + return ( + + + + + + + + + 0 && !isNameFormatVerified} + tip='Cookie names may only include ASCII characters without spaces, semicolons, or equals signs' + placement='top' + > + setCookieNameFocused(true)} + onBlur={() => setCookieNameFocused(false)} + showClearButton={false} + disabled={isPaused} + validationState={name.length > 0 && !isNameFormatVerified ? 'error' : 'default'} + /> + + + + + 0 && !isValueFormatVerified} + > + 0 && !isValueFormatVerified ? 'error' : 'default'} + /> + + + + } + onClick={() => onRequestCookiesRemoved([id])} + /> + + + + ); +} diff --git a/src/widgets/request-cookies/components/RequestCookieRow/styled.ts b/src/widgets/request-cookies/components/RequestCookieRow/styled.ts new file mode 100644 index 00000000..cc649d8a --- /dev/null +++ b/src/widgets/request-cookies/components/RequestCookieRow/styled.ts @@ -0,0 +1,49 @@ +import { CSS, type Transform } from '@dnd-kit/utilities'; +import styled from '@emotion/styled'; + +import { FieldText } from '@snack-uikit/fields'; +import { Droplist } from '@snack-uikit/list'; + +export const Wrapper = styled.div<{ transform: Transform | null; isDragging: boolean; transition?: string }>` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 4px; + + transform: ${props => CSS.Transform.toString(props.transform)}; + opacity: ${props => (props.isDragging ? 0 : 1)}; + transition: ${props => props.transition}; + + width: 100%; +`; + +export const LeftHeaderActions = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; +`; + +const legacyFieldStyles = ` + [data-test-id='field-container-private'] { + border-radius: 12px; + } +`; + +export const CookieFieldWrapper = styled.div<{ grow: number }>` + flex: ${props => props.grow} 1 0; + min-width: 0; +`; + +export const CookieNameField = styled(FieldText)` + ${legacyFieldStyles} +`; + +export const CookieValueField = styled(FieldText)` + ${legacyFieldStyles} +`; + +export const StyledDroplist = styled(Droplist)` + width: 228px; +`; diff --git a/src/widgets/request-cookies/index.ts b/src/widgets/request-cookies/index.ts new file mode 100644 index 00000000..0ac013a4 --- /dev/null +++ b/src/widgets/request-cookies/index.ts @@ -0,0 +1 @@ +export * from './RequestCookies'; diff --git a/src/widgets/request-cookies/styled.ts b/src/widgets/request-cookies/styled.ts new file mode 100644 index 00000000..61092c81 --- /dev/null +++ b/src/widgets/request-cookies/styled.ts @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + + gap: 8px; + align-items: flex-end; + padding-bottom: 16px; +`; diff --git a/tests/e2e/profiles.spec.ts b/tests/e2e/profiles.spec.ts index 486d1bef..e8b9dde9 100644 --- a/tests/e2e/profiles.spec.ts +++ b/tests/e2e/profiles.spec.ts @@ -106,13 +106,10 @@ test.describe('Profile Actions', () => { const profilesBefore = page.locator('[data-test-id="profile-select"]'); const countBefore = await profilesBefore.count(); - // Open the profile actions menu - await openProfileActionsMenu(page); - - // Select "Delete profile" - const deleteOption = page.getByRole('menuitem', { name: 'Delete profile' }); - await expect(deleteOption).toBeVisible({ timeout: 5000 }); - await deleteOption.click(); + // Delete the selected profile via the dedicated trash button + const removeProfileButton = page.locator('[data-test-id="remove-profile-button"]'); + await expect(removeProfileButton).toBeEnabled({ timeout: 5000 }); + await removeProfileButton.click(); // Wait for the profile to be removed const profilesAfter = page.locator('[data-test-id="profile-select"]'); @@ -228,19 +225,25 @@ test.describe('Profile Actions', () => { const jsonTextarea = page.locator('[data-test-id="import-profile-json-textarea"] textarea'); await expect(jsonTextarea).toBeVisible({ timeout: 10000 }); + // Mirror the real export/import shape: profiles carry no id/name, and headers/cookies/filters + // are stored without ids (ids are regenerated on import). const importJson = JSON.stringify([ { - id: 'imported-profile-1', - name: 'Imported Profile', + urlFilters: [], requestHeaders: [ { - id: 1, name: 'X-Imported-Header', value: 'imported-value', disabled: false, }, ], - urlFilters: [], + requestCookies: [ + { + name: 'imported_session', + value: 'cookie-value-123', + disabled: false, + }, + ], }, ]); @@ -253,6 +256,13 @@ test.describe('Profile Actions', () => { // Verify that the profile was imported (check header input) const headerNameField = page.locator('[data-test-id="header-name-input"] input'); await expect(headerNameField.first()).toHaveValue('X-Imported-Header', { timeout: 5000 }); + + // Verify that the imported request cookies were applied as well + await page.getByRole('tab', { name: 'Request cookies' }).click(); + const cookieNameField = page.locator('[data-test-id="cookie-name-input"] input'); + const cookieValueField = page.locator('[data-test-id="cookie-value-input"] input'); + await expect(cookieNameField.first()).toHaveValue('imported_session', { timeout: 5000 }); + await expect(cookieValueField.first()).toHaveValue('cookie-value-123'); }); /** diff --git a/tests/e2e/screenshot-tests/config/screenshot.config.ts b/tests/e2e/screenshot-tests/config/screenshot.config.ts index 93927201..6e578808 100644 --- a/tests/e2e/screenshot-tests/config/screenshot.config.ts +++ b/tests/e2e/screenshot-tests/config/screenshot.config.ts @@ -4,13 +4,14 @@ export const SCREENSHOT_CONFIG = { platform: process.env.PLATFORM || 'darwin', defaults: { caret: 'hide', - maxDiffPixels: 100, + maxDiffPixels: 150, timeout: 5000, threshold: 0.2, }, selectors: { tabs: { headers: '[role="tab"]:has-text("Headers")', + cookies: '[role="tab"]:has-text("Request cookies")', urlFilters: '[role="tab"]:has-text("URL Filters")', }, headers: { @@ -23,6 +24,17 @@ export const SCREENSHOT_CONFIG = { menuButton: '[data-test-id="request-header-menu-button"]', removeButton: '[data-test-id="remove-request-header-button"]', }, + cookies: { + section: '[data-test-id="profile-cookies-section"]', + addButton: '[data-test-id="add-request-cookie-button"]', + nameInput: '[data-test-id="cookie-name-input"] input', + valueInput: '[data-test-id="cookie-value-input"] input', + checkbox: '[data-test-id="request-cookie-checkbox"]', + toggleAllCheckbox: '[data-test-id="all-request-cookies-checkbox"]', + menuButton: '[data-test-id="request-cookie-menu-button"]', + removeButton: '[data-test-id="remove-request-cookie-button"]', + removeAllButton: '[data-test-id="remove-all-request-cookies-button"]', + }, urlFilters: { section: '[data-test-id="url-filters-section"]', input: '[data-test-id="url-filter-input"] input', diff --git a/tests/e2e/screenshot-tests/page-objects/cookies-tab.page.ts b/tests/e2e/screenshot-tests/page-objects/cookies-tab.page.ts new file mode 100644 index 00000000..75e9fbbd --- /dev/null +++ b/tests/e2e/screenshot-tests/page-objects/cookies-tab.page.ts @@ -0,0 +1,54 @@ +import type { Page } from '@playwright/test'; + +import { SCREENSHOT_CONFIG } from '../config/screenshot.config'; +import { safeClick, waitForVisible } from '../utils'; + +export class CookiesTabPage { + constructor(private readonly page: Page) {} + + async activate() { + const tab = this.page.locator(SCREENSHOT_CONFIG.selectors.tabs.cookies); + await safeClick(tab); + await waitForVisible(this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.section)); + } + + async addCookie(name: string, value: string, index = 0) { + await safeClick(this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.addButton)); + const nameInput = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.nameInput).nth(index); + const valueInput = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.valueInput).nth(index); + await waitForVisible(nameInput); + await nameInput.fill(name); + await valueInput.fill(value); + } + + async disableCookie(index = 0) { + const checkbox = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.checkbox).nth(index); + const checked = await checkbox.getAttribute('data-checked'); + if (checked !== 'false') { + await checkbox.click(); + } + } + + async toggleAllCookies(enabled: boolean) { + const checkbox = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.toggleAllCheckbox); + const checked = await checkbox.getAttribute('data-checked'); + if ((checked === 'true') !== enabled) { + await checkbox.click(); + } + } + + async openCookieMenu(index = 0) { + const menuButton = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.menuButton).nth(index); + await safeClick(menuButton); + } + + async removeCookie(index = 0) { + const removeButton = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.removeButton).nth(index); + await safeClick(removeButton); + } + + async openRemoveAllModal() { + const removeAllButton = this.page.locator(SCREENSHOT_CONFIG.selectors.cookies.removeAllButton); + await safeClick(removeAllButton); + } +} diff --git a/tests/e2e/screenshot-tests/page-objects/index.ts b/tests/e2e/screenshot-tests/page-objects/index.ts index ebbb902d..fc8d270a 100644 --- a/tests/e2e/screenshot-tests/page-objects/index.ts +++ b/tests/e2e/screenshot-tests/page-objects/index.ts @@ -1,3 +1,4 @@ +export * from './cookies-tab.page'; export * from './headers-tab.page'; export * from './modals.page'; export * from './popup.page'; diff --git a/tests/e2e/screenshot-tests/page-objects/popup.page.ts b/tests/e2e/screenshot-tests/page-objects/popup.page.ts index e4ac375e..6c99c377 100644 --- a/tests/e2e/screenshot-tests/page-objects/popup.page.ts +++ b/tests/e2e/screenshot-tests/page-objects/popup.page.ts @@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'; import { SCREENSHOT_CONFIG, type Theme } from '../config/screenshot.config'; import { ensureMenuClosed, safeClick, waitForNetworkIdle, waitForVisible } from '../utils'; +import { CookiesTabPage } from './cookies-tab.page'; import { HeadersTabPage } from './headers-tab.page'; import { SidebarPage } from './sidebar.page'; import { UrlFiltersTabPage } from './url-filters-tab.page'; @@ -57,6 +58,10 @@ export class PopupPage { return new HeadersTabPage(this.page); } + get cookiesTab() { + return new CookiesTabPage(this.page); + } + get urlFiltersTab() { return new UrlFiltersTabPage(this.page); } diff --git a/tests/e2e/screenshot-tests/specs/cookies.screenshots.ts b/tests/e2e/screenshot-tests/specs/cookies.screenshots.ts new file mode 100644 index 00000000..ffac0ceb --- /dev/null +++ b/tests/e2e/screenshot-tests/specs/cookies.screenshots.ts @@ -0,0 +1,90 @@ +import { SCREENSHOT_CONFIG } from '../config/screenshot.config'; +import { createScreenshotTest } from '../factories'; + +createScreenshotTest({ + area: 'cookies', + name: 'empty-state', + description: 'CloudHood Extension - Request cookies empty state', + setup: async popup => { + await popup.cookiesTab.activate(); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'single-cookie', + description: 'CloudHood Extension - Request cookies with single cookie', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('session_id', 'abc123'); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'multiple-cookies', + description: 'CloudHood Extension - Request cookies with multiple cookies', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('session_id', 'abc123', 0); + await popup.cookiesTab.addCookie('theme', 'dark', 1); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'disabled-cookie', + description: 'CloudHood Extension - Request cookies with disabled cookie', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('disabled_cookie', 'value'); + await popup.cookiesTab.disableCookie(0); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'all-disabled', + description: 'CloudHood Extension - All request cookies disabled', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('first_cookie', 'value-1', 0); + await popup.cookiesTab.addCookie('second_cookie', 'value-2', 1); + await popup.cookiesTab.toggleAllCookies(false); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'name-validation-error', + description: 'CloudHood Extension - Cookie name validation error', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('invalid name=', 'value', 0); + const nameInput = popup.page.locator(SCREENSHOT_CONFIG.selectors.cookies.nameInput).first(); + await nameInput.blur(); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'value-validation-error', + description: 'CloudHood Extension - Cookie value validation error', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('session_id', 'invalid;value', 0); + const valueInput = popup.page.locator(SCREENSHOT_CONFIG.selectors.cookies.valueInput).first(); + await valueInput.blur(); + }, +}); + +createScreenshotTest({ + area: 'cookies', + name: 'cookie-menu-open', + description: 'CloudHood Extension - Cookie menu open', + setup: async popup => { + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('menu_cookie', 'value', 0); + await popup.cookiesTab.openCookieMenu(0); + }, +}); diff --git a/tests/e2e/screenshot-tests/specs/modals.screenshots.ts b/tests/e2e/screenshot-tests/specs/modals.screenshots.ts index b7225638..e5619115 100644 --- a/tests/e2e/screenshot-tests/specs/modals.screenshots.ts +++ b/tests/e2e/screenshot-tests/specs/modals.screenshots.ts @@ -53,3 +53,16 @@ createScreenshotTest({ await modals.waitForTitle('Remove all URL filters'); }, }); + +createScreenshotTest({ + area: 'modals', + name: 'cookies-delete-confirmation', + description: 'CloudHood Extension - Remove all request cookies confirmation', + setup: async popup => { + const modals = new ModalsPage(popup.page); + await popup.cookiesTab.activate(); + await popup.cookiesTab.addCookie('session_id', 'abc123'); + await popup.cookiesTab.openRemoveAllModal(); + await modals.waitForTitle('Remove all request cookies'); + }, +}); diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-all-disabled-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-all-disabled-dark.png new file mode 100644 index 00000000..573ec0d7 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-all-disabled-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-all-disabled-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-all-disabled-light.png new file mode 100644 index 00000000..a11c36d3 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-all-disabled-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-cookie-menu-open-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-cookie-menu-open-dark.png new file mode 100644 index 00000000..2c2f9808 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-cookie-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-cookie-menu-open-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-cookie-menu-open-light.png new file mode 100644 index 00000000..07e00355 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-cookie-menu-open-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-disabled-cookie-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-disabled-cookie-dark.png new file mode 100644 index 00000000..af198bc0 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-disabled-cookie-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-disabled-cookie-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-disabled-cookie-light.png new file mode 100644 index 00000000..d919287f Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-disabled-cookie-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-empty-state-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-empty-state-dark.png new file mode 100644 index 00000000..f23135af Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-empty-state-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-empty-state-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-empty-state-light.png new file mode 100644 index 00000000..8b5dcbf6 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-empty-state-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-multiple-cookies-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-multiple-cookies-dark.png new file mode 100644 index 00000000..54fb39c5 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-multiple-cookies-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-multiple-cookies-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-multiple-cookies-light.png new file mode 100644 index 00000000..85932848 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-multiple-cookies-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-name-validation-error-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-name-validation-error-dark.png new file mode 100644 index 00000000..85fc9b31 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-name-validation-error-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-name-validation-error-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-name-validation-error-light.png new file mode 100644 index 00000000..30e30448 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-name-validation-error-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-single-cookie-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-single-cookie-dark.png new file mode 100644 index 00000000..0c183495 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-single-cookie-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-single-cookie-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-single-cookie-light.png new file mode 100644 index 00000000..de2ecd5d Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-single-cookie-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-value-validation-error-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-value-validation-error-dark.png new file mode 100644 index 00000000..3ad2bfcc Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-value-validation-error-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-value-validation-error-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-value-validation-error-light.png new file mode 100644 index 00000000..3b18b230 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/cookies-value-validation-error-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-dark.png index 7449f186..b5f04448 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-light.png index 4e8df85d..55d31c94 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-empty-popup-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-dark.png index abecfbfe..818dde3b 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-light.png index 86912c8d..08270a68 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-paused-state-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-dark.png index a326eb10..0daa5b13 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-light.png index 1735ad65..3d4f64e1 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/general-theme-menu-open-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-dark.png index 5280cbbf..068cf795 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-light.png index 6e0d9395..649fb39a 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-all-disabled-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-dark.png index 1a0c6735..9f36c54a 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-light.png index 2fae2211..8bc58cf4 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-disabled-header-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-dark.png index 7449f186..b5f04448 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-light.png index 4e8df85d..55d31c94 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-empty-state-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-dark.png index 63c4ecf7..3c11e85e 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-light.png index 934bdc31..468ef0fa 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-header-menu-open-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-dark.png index dd5a3a22..07a8a412 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-light.png index bfd241d9..025a1317 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-multiple-headers-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-dark.png index 59e56a12..c4602710 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-light.png index c1afee8a..e5e4592d 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-single-header-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-dark.png index d909d59a..caa5ed84 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-light.png index 41bbe986..3e390710 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/headers-validation-error-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-cookies-delete-confirmation-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-cookies-delete-confirmation-dark.png new file mode 100644 index 00000000..0ceb9214 Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-cookies-delete-confirmation-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-cookies-delete-confirmation-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-cookies-delete-confirmation-light.png new file mode 100644 index 00000000..ac9d772a Binary files /dev/null and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-cookies-delete-confirmation-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-dark.png index 83fb01b1..e9cfd9dc 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-light.png index 37f01b4b..b4e8b669 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-delete-confirmation-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-dark.png index cc615edb..1539e7ec 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-light.png index 1a06946b..780cf46c 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-export-modal-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-dark.png index 27ac2368..3429effe 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-light.png index 89f90cd9..56024e5b 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-from-extension-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-dark.png index 6fb0070a..73d4f16c 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-light.png index 1b4b886c..536bc477 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/modals-import-modal-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-dark.png index b83f16a9..d8356042 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-light.png index b6088ffb..6fc4def3 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-multiple-profiles-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-dark.png index 7ebc0d13..5054a547 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-light.png index 1ad2a1b4..fe6d77ae 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-actions-menu-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-dark.png index 986af6f7..65b78ab7 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-light.png index 95678d81..eb7dc4ff 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-editing-name-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-dark.png index b83f16a9..d8356042 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-light.png index b6088ffb..6fc4def3 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-profile-selected-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-dark.png index 7449f186..b5f04448 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-light.png index 4e8df85d..55d31c94 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/profiles-single-profile-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-dark.png index c09bfebc..705422e7 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-light.png index 63ee9941..5cba7344 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-disabled-filter-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-dark.png index 074fe482..8c188015 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-light.png index 24ecb15c..d5376390 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-empty-state-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-dark.png index 706e5cbf..1d03e1dd 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-light.png index 414253da..f5af4a6c 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-filter-menu-open-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-dark.png index 2d6bf08a..17eaae54 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-light.png index 560905da..a780dd11 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-multiple-filters-light.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-dark.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-dark.png index 8944ed30..430f6960 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-dark.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-dark.png differ diff --git a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-light.png b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-light.png index 02862962..b8d0f26e 100644 Binary files a/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-light.png and b/tests/e2e/screenshots.firefox.spec.ts-snapshots/url-filters-single-filter-light.png differ diff --git a/tests/e2e/screenshots.spec.ts b/tests/e2e/screenshots.spec.ts index 71349e70..f6a8afd5 100644 --- a/tests/e2e/screenshots.spec.ts +++ b/tests/e2e/screenshots.spec.ts @@ -1,3 +1,4 @@ +import './screenshot-tests/specs/cookies.screenshots'; import './screenshot-tests/specs/general-ui.screenshots'; import './screenshot-tests/specs/headers.screenshots'; import './screenshot-tests/specs/modals.screenshots'; diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-all-disabled-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-all-disabled-dark.png new file mode 100644 index 00000000..e4d62887 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-all-disabled-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-all-disabled-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-all-disabled-light.png new file mode 100644 index 00000000..43248955 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-all-disabled-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-cookie-menu-open-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-cookie-menu-open-dark.png new file mode 100644 index 00000000..791124b9 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-cookie-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-cookie-menu-open-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-cookie-menu-open-light.png new file mode 100644 index 00000000..17a54cc0 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-cookie-menu-open-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-disabled-cookie-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-disabled-cookie-dark.png new file mode 100644 index 00000000..8a8d5430 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-disabled-cookie-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-disabled-cookie-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-disabled-cookie-light.png new file mode 100644 index 00000000..d65b2463 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-disabled-cookie-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-empty-state-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-empty-state-dark.png new file mode 100644 index 00000000..d71f1fea Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-empty-state-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-empty-state-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-empty-state-light.png new file mode 100644 index 00000000..e49d2fac Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-empty-state-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-multiple-cookies-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-multiple-cookies-dark.png new file mode 100644 index 00000000..abebc20b Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-multiple-cookies-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-multiple-cookies-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-multiple-cookies-light.png new file mode 100644 index 00000000..65bdf5f5 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-multiple-cookies-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-name-validation-error-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-name-validation-error-dark.png new file mode 100644 index 00000000..92bfe84a Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-name-validation-error-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-name-validation-error-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-name-validation-error-light.png new file mode 100644 index 00000000..0be42994 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-name-validation-error-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-single-cookie-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-single-cookie-dark.png new file mode 100644 index 00000000..26bfee2c Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-single-cookie-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-single-cookie-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-single-cookie-light.png new file mode 100644 index 00000000..44d623c2 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-single-cookie-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-value-validation-error-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-value-validation-error-dark.png new file mode 100644 index 00000000..32de941c Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-value-validation-error-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/cookies-value-validation-error-light.png b/tests/e2e/screenshots.spec.ts-snapshots/cookies-value-validation-error-light.png new file mode 100644 index 00000000..bc19e599 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/cookies-value-validation-error-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-dark.png index 0c5350a7..67cdcbac 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-light.png b/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-light.png index 52934ac2..59dac483 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/general-empty-popup-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-dark.png index 4f9d7bf5..2891a340 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-light.png b/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-light.png index 48b43b96..13340cc7 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/general-paused-state-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-dark.png index b793476e..eac81782 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-light.png b/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-light.png index e020cd7f..538a1f2a 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/general-theme-menu-open-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-dark.png index c277251a..88462a71 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-light.png index beff09bf..cdb97607 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-all-disabled-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-dark.png index 96d36a1c..1b24b57b 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-light.png index f2dfcc5d..fbd6cc83 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-disabled-header-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-dark.png index 0c5350a7..67cdcbac 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-light.png index 52934ac2..59dac483 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-empty-state-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-dark.png index e4c75b29..43ddacc9 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-light.png index 716a6cca..8c03781e 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-header-menu-open-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-dark.png index 6d24c74c..5e189a43 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-light.png index fbad163c..056e82a2 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-multiple-headers-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-dark.png index 124bcf8f..f51bb868 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-light.png index 8f1a1310..7d0ea243 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-single-header-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-dark.png index d33ad5d9..d77d14e2 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-light.png b/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-light.png index 40acdd11..43ac9131 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/headers-validation-error-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-cookies-delete-confirmation-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-cookies-delete-confirmation-dark.png new file mode 100644 index 00000000..aae3a648 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/modals-cookies-delete-confirmation-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-cookies-delete-confirmation-light.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-cookies-delete-confirmation-light.png new file mode 100644 index 00000000..29d22ce4 Binary files /dev/null and b/tests/e2e/screenshots.spec.ts-snapshots/modals-cookies-delete-confirmation-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-dark.png index bf7f83a0..d7875649 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-light.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-light.png index 6ebb61ba..87c39260 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-delete-confirmation-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-dark.png index e6a48c54..e66c4c94 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-light.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-light.png index 69a764ae..62af0652 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-export-modal-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-import-from-extension-light.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-import-from-extension-light.png index 19ae776a..8b970950 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-import-from-extension-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-import-from-extension-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-dark.png index 3f1c8747..c5676fcb 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-light.png b/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-light.png index 57db74e6..47fbc411 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/modals-import-modal-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-dark.png index 93425526..aa607830 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-light.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-light.png index 4be70d70..d9025a96 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-multiple-profiles-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-dark.png index f8b8a1fc..e793dfb8 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-light.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-light.png index e886f480..fd7e7fe9 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-actions-menu-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-dark.png index 3b7a9dee..7493b478 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-light.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-light.png index 40e18b9b..7ddd7194 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-editing-name-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-dark.png index b2df2b92..40a1ae4b 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-light.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-light.png index 3b86b0ea..87e9fa89 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-profile-selected-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-dark.png index 0c5350a7..67cdcbac 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-light.png b/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-light.png index 0c963c65..2c876ab3 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/profiles-single-profile-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-dark.png index a092a996..831cab3b 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-light.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-light.png index bbc52e39..06a98ad5 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-disabled-filter-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-dark.png index 3a216ab4..a7717e5a 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-light.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-light.png index 51a79c88..22122d78 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-empty-state-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-dark.png index 0b4bfeb6..02079118 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-light.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-light.png index ed113ff1..165c4178 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-filter-menu-open-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-dark.png index 21265f8d..70f3593f 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-light.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-light.png index df7ebd5d..b0fde449 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-multiple-filters-light.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-dark.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-dark.png index 4a50ec6b..ed10d4cd 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-dark.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-dark.png differ diff --git a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-light.png b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-light.png index 79eee9e9..5d0c8804 100644 Binary files a/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-light.png and b/tests/e2e/screenshots.spec.ts-snapshots/url-filters-single-filter-light.png differ