) => void
+ testId?: string
}
export function PageActionItem(props: Props): JSX.Element {
- const { step, currentId, failedId, failedMessage } = props
+ const { step, currentId, failedId, failedMessage, testId } = props
const [isOpen, setIsOpen] = useState(false)
const [shouldRender, setShouldRender] = useState(false)
const [isEditing, setIsEditing] = useState(false)
@@ -134,6 +135,7 @@ export function PageActionItem(props: Props): JSX.Element {
)}
ref={anchorRef}
{...onHover(onHoverTrigger, true)}
+ data-testid={testId}
>
{/* List icon */}
{}
@@ -43,6 +44,7 @@ export function StepList(props: Props): JSX.Element {
onClickRemove={onClickRemove}
onClickEdit={onClickEdit}
onChange={onChange}
+ testId={TEST_IDS.pageActionStep(step.param.type)}
className={cn(
"relative",
i > 0 &&
diff --git a/packages/extension/src/const.ts b/packages/extension/src/const.ts
index e388b17d..6ed5330c 100644
--- a/packages/extension/src/const.ts
+++ b/packages/extension/src/const.ts
@@ -2,13 +2,15 @@ import { OPEN_MODE } from "@shared"
export { OPEN_MODE, PAGE_ACTION_OPEN_MODE, SEARCH_OPEN_MODE } from "@shared"
export const APP_ID = "selection-command"
-export const VERSION = __APP_VERSION__ as string
+export const VERSION = (
+ typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0"
+) as string
/**
* Setting value to switch the debug log output from this module.
* true: enables all log. | false: disables debug log.
*/
-const environment = import.meta.env.MODE ?? "development"
+const environment = import.meta.env?.MODE ?? "development"
export const isDebug = environment === "development"
export const isE2E = environment === "e2e"
diff --git a/packages/extension/src/content_script.tsx b/packages/extension/src/content_script.tsx
index 70ddd7a6..3a978a65 100644
--- a/packages/extension/src/content_script.tsx
+++ b/packages/extension/src/content_script.tsx
@@ -39,6 +39,7 @@ try {
if (!isDebug) {
// Putting styles into ShadowDom
+ insertCss(shadow, "/assets/components.css")
insertCss(shadow, "/assets/content_script.css")
}
diff --git a/packages/extension/src/services/backgroundData.ts b/packages/extension/src/services/backgroundData.ts
index 8630f779..947d1a5f 100644
--- a/packages/extension/src/services/backgroundData.ts
+++ b/packages/extension/src/services/backgroundData.ts
@@ -17,7 +17,6 @@ export class BgData {
public windowStack: WindowLayer[]
public normalWindows: WindowLayer
public pageActionStop: boolean
- public activeScreenId: string | null
public activeTabId: number | null
public connectedTabs: number[]
public sidePanelTabs: SidePanelTab[]
@@ -27,7 +26,6 @@ export class BgData {
this.windowStack = val?.windowStack ?? []
this.normalWindows = val?.normalWindows ?? []
this.pageActionStop = val?.pageActionStop ?? false
- this.activeScreenId = val?.activeScreenId ?? null
this.activeTabId = val?.activeTabId ?? null
this.connectedTabs = val?.connectedTabs ?? []
// Normalize sidePanelTabs: convert legacy number[] entries to SidePanelTab objects
diff --git a/packages/extension/src/services/chrome.ts b/packages/extension/src/services/chrome.ts
index e7dbba24..e2d597d0 100644
--- a/packages/extension/src/services/chrome.ts
+++ b/packages/extension/src/services/chrome.ts
@@ -2,9 +2,11 @@ import { sleep, toUrl, isOverBytes, isUrlParam } from "@/lib/utils"
import type { ScreenSize } from "@/services/dom"
import type { ShowToastParam, UrlParam, WindowLayer } from "@/types"
import { POPUP_OFFSET, POPUP_TYPE, WINDOW_STATE } from "@/const"
+import { PopupOption } from "@/services/option/defaultSettings"
import { BgData } from "@/services/backgroundData"
import { WindowStackManager } from "@/services/windowStackManager"
import { BgCommand, ClipboardResult, TabCommand } from "@/services/ipc"
+import { getScreenSize } from "@/services/screen"
import { Ipc } from "@/services/ipc"
import { t } from "@/services/i18n"
@@ -103,9 +105,8 @@ export type OpenPopupProps = {
url: string | UrlParam
top: number
left: number
- width: number
- height: number
- screen: ScreenSize
+ width: number | undefined
+ height: number | undefined
type: POPUP_TYPE
windowState?: WINDOW_STATE
}
@@ -115,12 +116,15 @@ export type OpenPopupsProps = {
urls: string[]
top: number
left: number
- width: number
- height: number
- screen: ScreenSize
+ width: number | undefined
+ height: number | undefined
type: POPUP_TYPE
}
+export type OpenPopupAndClickProps = OpenPopupProps & {
+ selector: string
+}
+
export type OpenTabProps = {
url: string | UrlParam
active: boolean
@@ -155,23 +159,27 @@ type OpenResult = {
const adjustWindowPosition = (
top: number,
left: number,
- width: number,
- height: number,
+ width: number | undefined,
+ height: number | undefined,
screen: ScreenSize,
offset: number = 0,
): { top: number; left: number } => {
let t = top + POPUP_OFFSET * offset
let l = left + POPUP_OFFSET * offset
- if (screen.height < t + height - screen.top) {
+ // Use default height and width if not provided or invalid
+ const _height = height && height > 0 ? height : PopupOption.height
+ const _width = width && width > 0 ? width : PopupOption.width
+
+ if (screen.height < t + _height - screen.top) {
t =
- Math.floor((screen.height - height) / 2) +
+ Math.floor((screen.height - _height) / 2) +
screen.top +
POPUP_OFFSET * offset
}
- if (screen.width < l + width - screen.left) {
+ if (screen.width < l + _width - screen.left) {
l =
- Math.floor((screen.width - width) / 2) +
+ Math.floor((screen.width - _width) / 2) +
screen.left +
POPUP_OFFSET * offset
}
@@ -429,7 +437,7 @@ export const readClipboard = async (): Promise<{
export const openPopupWindow = async (
param: OpenPopupProps,
): Promise => {
- const { top, left, width, height, screen, url } = param
+ const { top, left, width, height, url } = param
let current: chrome.windows.Window
try {
@@ -438,6 +446,8 @@ export const openPopupWindow = async (
current = { id: undefined, incognito: false } as chrome.windows.Window
}
+ const screenSize = await getScreenSize()
+
const type = param.type ?? POPUP_TYPE.POPUP
const isFullscreen = param.windowState === WINDOW_STATE.FULLSCREEN
const isMaximized = param.windowState === WINDOW_STATE.MAXIMIZED
@@ -446,7 +456,7 @@ export const openPopupWindow = async (
left,
width,
height,
- screen,
+ screenSize,
)
const usesWindowState = isFullscreen || isMaximized
@@ -462,7 +472,6 @@ export const openPopupWindow = async (
if (isUrlParam(url) && url.useClipboard) {
const result = await openWindowAndReadClipboard({
commandId: param.commandId,
- screen: param.screen,
type,
width,
height,
@@ -535,7 +544,7 @@ export const openPopupWindow = async (
export const openPopupWindowMultiple = async (
param: OpenPopupsProps,
): Promise => {
- const { top, left, width, height, screen } = param
+ const { top, left, width, height } = param
let current: chrome.windows.Window
try {
current = await chrome.windows.getCurrent()
@@ -544,6 +553,8 @@ export const openPopupWindowMultiple = async (
}
const type = param.type ?? POPUP_TYPE.POPUP
+ const screenSize = await getScreenSize()
+
const windows = await Promise.all(
param.urls
.reverse()
@@ -553,7 +564,7 @@ export const openPopupWindowMultiple = async (
left,
width,
height,
- screen,
+ screenSize,
idx,
)
return chrome.windows.create({
diff --git a/packages/extension/src/services/pageAction/background-shared.ts b/packages/extension/src/services/pageAction/background-shared.ts
index 9ee2f4f6..5131b8b3 100644
--- a/packages/extension/src/services/pageAction/background-shared.ts
+++ b/packages/extension/src/services/pageAction/background-shared.ts
@@ -59,6 +59,7 @@ vi.mock("@/lib/utils", () => ({
generateRandomID: vi.fn(() => "test-id"),
isEmpty: vi.fn(),
isPageActionCommand: vi.fn(),
+ isServiceWorker: vi.fn(() => true),
isUrl: vi.fn(),
isUrlParam: vi.fn(),
matchesPageActionUrl: vi.fn(),
@@ -103,7 +104,12 @@ vi.mock("@/const", async () => {
// Import modules after mocking
import { Storage } from "@/services/storage"
import { Ipc } from "@/services/ipc"
-import { openPopupWindow, openTab, getCurrentTab, readClipboard } from "@/services/chrome"
+import {
+ openPopupWindow,
+ openTab,
+ getCurrentTab,
+ readClipboard,
+} from "@/services/chrome"
import { BgData } from "@/services/backgroundData"
import { RunningStatus } from "@/services/pageAction"
import { incrementCommandExecutionCount } from "@/services/commandMetrics"
diff --git a/packages/extension/src/services/pageAction/background.ts b/packages/extension/src/services/pageAction/background.ts
index de47f1c0..715d5eb8 100644
--- a/packages/extension/src/services/pageAction/background.ts
+++ b/packages/extension/src/services/pageAction/background.ts
@@ -8,7 +8,7 @@ import {
import { Storage, SESSION_STORAGE_KEY } from "@/services/storage"
import type { PageAction } from "@/services/pageAction"
import { RunningStatus } from "@/services/pageAction"
-import { ScreenSize } from "@/services/dom"
+import { getScreenSize, ScreenSize } from "@/services/screen"
import {
openPopupWindow,
openTab,
@@ -637,13 +637,14 @@ export const openRecorder = (
startUrl: string
openMode: PAGE_ACTION_OPEN_MODE
size: PopupOption
- screen: ScreenSize
+ screen?: ScreenSize
},
sender: Sender,
response: (res: unknown) => void,
): boolean => {
- const { startUrl, openMode, size, screen } = param
+ const { startUrl, openMode, size } = param
const open = async () => {
+ const screen = param.screen ?? (await getScreenSize())
const t = Math.floor((screen.height - size.height) / 2) + screen.top
const l = Math.floor((screen.width - size.width) / 2) + screen.left
try {
diff --git a/packages/extension/src/services/screen.ts b/packages/extension/src/services/screen.ts
index 5f942e1c..63f1bbcd 100644
--- a/packages/extension/src/services/screen.ts
+++ b/packages/extension/src/services/screen.ts
@@ -1,5 +1,7 @@
-import { BgData } from "@/services/backgroundData"
-import { windowExists } from "@/services/chrome"
+import { isServiceWorker } from "@/lib/utils"
+
+const DEFAULT_SCREEN_WIDTH = 1280
+const DEFAULT_SCREEN_HEIGHT = 800
type WindowPosition = {
top: number
@@ -13,40 +15,6 @@ export type ScreenSize = {
top: number
}
-export async function updateActiveScreenId(windowId: number): Promise {
- try {
- const result = await windowExists(windowId)
- if (!result.exists) {
- console.warn(`Window ${windowId} does not exist for screen ID update`)
- return
- }
-
- const left = result.window.left ?? 0
- const top = result.window.top ?? 0
-
- // Find the display that contains the window
- const displays = await chrome.system.display.getInfo()
- const display = displays.find((d) => {
- return (
- left >= d.bounds.left &&
- left < d.bounds.left + d.bounds.width &&
- top >= d.bounds.top &&
- top < d.bounds.top + d.bounds.height
- )
- })
-
- if (display) {
- // Update BgData with the active screen ID
- await BgData.set((data) => ({
- ...data,
- activeScreenId: display.id,
- }))
- }
- } catch (error) {
- console.warn("Failed to update active screen ID:", error)
- }
-}
-
export async function getWindowPosition(): Promise {
// Get window position
let top = 0
@@ -70,29 +38,72 @@ export async function getWindowPosition(): Promise {
}
export async function getScreenSize(): Promise {
- try {
- // For background_script.ts
- const displays = await chrome.system.display.getInfo()
- const activeScreenId = BgData.get().activeScreenId
+ if (isServiceWorker()) {
+ try {
+ // For background_script.ts
+ const [displays, currentWindow] = await Promise.all([
+ chrome.system.display.getInfo(),
+ chrome.windows.getCurrent(),
+ ])
- // Use the screen with active screen ID if it exists,
- // otherwise use the primary display
- const targetDisplay = activeScreenId
- ? displays.find((d) => d.id === activeScreenId)
- : displays.find((d) => d.isPrimary)
+ let targetDisplay
+ const currentWindowLeft = currentWindow.left
+ const currentWindowTop = currentWindow.top
- if (!targetDisplay) {
- throw new Error("Target display not found")
- }
+ if (currentWindowLeft != null && currentWindowTop != null) {
+ // Find the monitor that contains the active window's left position
+ targetDisplay =
+ displays.find((d) => {
+ const a = d.workArea
+ return (
+ currentWindowLeft >= a.left &&
+ currentWindowLeft < a.left + a.width &&
+ currentWindowTop >= a.top &&
+ currentWindowTop < a.top + a.height
+ )
+ }) ??
+ displays.find((d) => d.isPrimary) ??
+ displays[0]
+ } else {
+ // If left/top cannot be retrieved, prefer returning the primary monitor
+ targetDisplay = displays.find((d) => d.isPrimary) ?? displays[0]
+ }
- return {
- width: targetDisplay.bounds.width,
- height: targetDisplay.bounds.height,
- left: targetDisplay.bounds.left,
- top: targetDisplay.bounds.top,
+ if (!targetDisplay) {
+ throw new Error("Target display not found")
+ }
+
+ return {
+ width: targetDisplay.bounds.width,
+ height: targetDisplay.bounds.height,
+ left: targetDisplay.bounds.left,
+ top: targetDisplay.bounds.top,
+ }
+ } catch (error) {
+ console.warn(
+ "Failed to get screen size in service worker, using fallback:",
+ error,
+ )
+ // Fallback: use the current window's size as a minimum screen estimate
+ try {
+ const w = await chrome.windows.getCurrent()
+ return {
+ width: w.width ?? DEFAULT_SCREEN_WIDTH,
+ height: w.height ?? DEFAULT_SCREEN_HEIGHT,
+ left: 0,
+ top: 0,
+ }
+ } catch (fallbackError) {
+ console.warn("Fallback screen size estimation failed:", fallbackError)
+ return {
+ width: DEFAULT_SCREEN_WIDTH,
+ height: DEFAULT_SCREEN_HEIGHT,
+ left: 0,
+ top: 0,
+ }
+ }
}
- } catch (error) {
- // For tabs
+ } else {
return {
width: window.screen.width,
height: window.screen.height,
diff --git a/packages/extension/src/services/windowStackManager.test.ts b/packages/extension/src/services/windowStackManager.test.ts
index a76b54ff..f9986954 100644
--- a/packages/extension/src/services/windowStackManager.test.ts
+++ b/packages/extension/src/services/windowStackManager.test.ts
@@ -32,7 +32,6 @@ describe("WindowStackManager", () => {
windowStack,
normalWindows: [],
pageActionStop: false,
- activeScreenId: null,
activeTabId: null,
connectedTabs: [],
sidePanelTabs: [],
diff --git a/packages/extension/src/testIds.ts b/packages/extension/src/testIds.ts
index 4c7dd49c..6b59f06e 100644
--- a/packages/extension/src/testIds.ts
+++ b/packages/extension/src/testIds.ts
@@ -4,4 +4,20 @@ export const TEST_IDS = {
importFileInput: "import-file-input",
optionDialogOk: "option-dialog-ok",
optionDialogCancel: "option-dialog-cancel",
+ exportButton: "export-button",
+ resetButton: "reset-button",
+ userStyleAddButton: "user-style-add-button",
+ userStyleSaveButton: "user-style-save-button",
+ userStyleItem: "user-style-item",
+ userStyleEditButton: "user-style-edit-button",
+ userStyleRemoveButton: "user-style-remove-button",
+ userStyleRemoveOkButton: "user-style-remove-ok-button",
+ addFolderButton: "add-folder-button",
+ addCommandButton: "add-command-button",
+ commandType: (type: string) => `command-type-${type}`,
+ recButton: "rec-button",
+ pageActionCompleteButton: "page-action-complete-button",
+ pageActionStep: (type: string) => `page-action-step-${type}`,
+ selectTrigger: (name: string) => `select-trigger-${name.replace(/\./g, "-")}`,
+ selectItem: (name: string) => `select-item-${name.replace(/\./g, "-")}`,
}
diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts
index 3dc1bfe9..07e62fb3 100644
--- a/packages/extension/vite.config.ts
+++ b/packages/extension/vite.config.ts
@@ -123,20 +123,20 @@ export default defineConfig(({ mode }) => {
pure:
mode === "production"
? [
- "console.log",
- "console.debug",
- "console.info",
- "console.trace",
- "console.dir",
- "console.count",
- "console.countReset",
- "console.group",
- "console.groupCollapsed",
- "console.groupEnd",
- "console.time",
- "console.timeEnd",
- "console.timeLog",
- ]
+ "console.log",
+ "console.debug",
+ "console.info",
+ "console.trace",
+ "console.dir",
+ "console.count",
+ "console.countReset",
+ "console.group",
+ "console.groupCollapsed",
+ "console.groupEnd",
+ "console.time",
+ "console.timeEnd",
+ "console.timeLog",
+ ]
: [],
},
build: {
@@ -147,12 +147,21 @@ export default defineConfig(({ mode }) => {
clipboard: "src/clipboard.html",
},
output: {
+ // Group all CSS module files into a named chunk so that the
+ // extracted CSS asset gets a predictable name ("components.css").
+ // This avoids content-based heuristics in assetFileNames.
+ manualChunks(id) {
+ if (/\.module\.css(\?.*)?$/.test(id)) {
+ return "components"
+ }
+ },
assetFileNames: (assetInfo) => {
- const keepNames = [
- "content_script.css",
- "icons.css",
- "command_hub.css",
- ]
+ // Filename-based detection: CSS modules are grouped into the
+ // "components" chunk above, so their CSS asset is named "components.css".
+ if (assetInfo.names?.[0] === "components.css") {
+ return `assets/components.css`
+ }
+ const keepNames = ["content_script.css", "command_hub.css"]
if (
assetInfo.names?.length > 0 &&
keepNames.includes(assetInfo.names[0])
diff --git a/vitest.config.ts b/vitest.config.ts
index a18e5899..d08973ca 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from "vitest/config";
+import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
@@ -16,7 +16,10 @@ export default defineConfig({
"**/build/**",
"**/.next/**",
"**/coverage/**",
+ "**/e2e/**",
+ "**/scripts/**",
+ "**/*.config.ts",
],
},
},
-});
+})