Upgrade to Manifest V3 (#1714)

* Update Chrome manifest.json

* Remove use of window in background

* Test devpanel

* Inject pageScript using new API

* Keep connection from devpanel to background alive

* Keep connection from content script to background alive

* Replace page action with action

* Cleanup syncOptions

* Update options to not rely on background page access

* Start work on updating popup

* Updates

* Remove window

* Get opening in a separate window working

* Remove pageScriptWrap

* Add socket to panelStore

* Fix tests

* Try to use MV3 for Firefox

* Fix path

* Fix Chrome E2E tests

* Revert unintentional change

* Skip Electron tests for now

Looks like they're still working through stuff in https://github.com/electron/electron/issues/41613

* Better image centering

The Firefox popup did not like the old CSS. This is still not perfect, but it's better than it was.

* Create shaggy-taxis-cross.md
This commit is contained in:
Nathan Bierema 2024-08-17 15:11:46 -04:00 committed by GitHub
parent 61ec00f505
commit 83b2c19a11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 347 additions and 766 deletions

View File

@ -0,0 +1,5 @@
---
'remotedev-redux-devtools-extension': minor
---
Upgrade to Manifest V3

View File

@ -5,7 +5,7 @@ import pug from 'pug';
const args = process.argv.slice(2);
const prod = !args.includes('--dev');
const commonEsbuildOptions = {
await esbuild.build({
bundle: true,
logLevel: 'info',
outdir: 'dist',
@ -15,40 +15,24 @@ const commonEsbuildOptions = {
'process.env.NODE_ENV': prod ? '"production"' : '"development"',
'process.env.BABEL_ENV': prod ? '"production"' : '"development"',
},
};
await esbuild.build({
...commonEsbuildOptions,
entryPoints: [
{ out: 'background.bundle', in: 'src/background/index.ts' },
{ out: 'options.bundle', in: 'src/options/index.tsx' },
{ out: 'window.bundle', in: 'src/window/index.tsx' },
{ out: 'remote.bundle', in: 'src/remote/index.tsx' },
{ out: 'devpanel.bundle', in: 'src/devpanel/index.tsx' },
{ out: 'devtools.bundle', in: 'src/devtools/index.ts' },
{ out: 'content.bundle', in: 'src/contentScript/index.ts' },
{ out: 'page.bundle', in: 'src/pageScript/index.ts' },
...(prod ? [] : [{ out: 'pagewrap.bundle', in: 'src/pageScriptWrap.ts' }]),
],
loader: {
'.woff2': 'file',
},
});
if (prod) {
await esbuild.build({
...commonEsbuildOptions,
entryPoints: [{ out: 'pagewrap.bundle', in: 'src/pageScriptWrap.ts' }],
loader: {
'.js': 'text',
},
});
}
console.log();
console.log('Creating HTML files...');
const htmlFiles = ['devpanel', 'devtools', 'options', 'remote', 'window'];
const htmlFiles = ['devpanel', 'devtools', 'options', 'remote'];
for (const htmlFile of htmlFiles) {
fs.writeFileSync(
`dist/${htmlFile}.html`,

View File

@ -3,26 +3,20 @@
"name": "Redux DevTools",
"description": "Redux DevTools for debugging application's state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"manifest_version": 2,
"page_action": {
"manifest_version": 3,
"action": {
"default_icon": "img/logo/gray.png",
"default_title": "Redux DevTools",
"default_popup": "window.html#popup"
"default_popup": "devpanel.html#popup"
},
"commands": {
"devtools-left": {
"description": "DevTools window to left"
},
"devtools-right": {
"description": "DevTools window to right"
},
"devtools-bottom": {
"description": "DevTools window to bottom"
"devtools-window": {
"description": "DevTools window"
},
"devtools-remote": {
"description": "Remote DevTools"
},
"_execute_page_action": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+E"
}
@ -34,36 +28,37 @@
"128": "img/logo/128x128.png"
},
"options_ui": {
"page": "options.html",
"chrome_style": true
"page": "options.html"
},
"background": {
"scripts": ["background.bundle.js"],
"persistent": false
"service_worker": "background.bundle.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["content.bundle.js", "pagewrap.bundle.js"],
"js": ["content.bundle.js"],
"run_at": "document_start",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["page.bundle.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
}
],
"devtools_page": "devtools.html",
"web_accessible_resources": ["page.bundle.js"],
"externally_connectable": {
"ids": ["*"]
},
"permissions": [
"notifications",
"contextMenus",
"storage",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;",
"permissions": ["notifications", "contextMenus", "storage"],
"host_permissions": ["file:///*", "http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;"
},
"update_url": "https://clients2.google.com/service/update2/crx",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdJEPwY92xUACA9CcDBDBmbdbp8Ap3cKQ0DJTUuVQvqb4FQAv8RtKY3iUjGvdwuAcSJQIZwHXcP2aNDH3TiFik/NhRK2GRW8X3OZyTdkuDueABGP2KEX8q1WQDgjX/rPIinGYztUrvoICw/UerMPwNW62jwGoVU3YhAGf+15CgX2Y6a4tppnf/+1mPedKPidh0RsM+aJY98rX+r1SPAHPcGzMjocLkqcT75DZBXer8VQN14tOOzRCd6T6oy7qm7eWru8lJwcY66qMQvhk0osqEod2G3nA7aTWpmqPFS66VEiecP9PgZlp8gQdgZ3dFhA62exydlD55JuRhiMIR63yQIDAQAB"
}

View File

@ -1,28 +1,22 @@
{
"version": "3.1.10",
"name": "Redux DevTools",
"manifest_version": 2,
"manifest_version": 3,
"description": "Redux Developer Tools for debugging application state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"applications": {
"browser_specific_settings": {
"gecko": {
"id": "extension@redux.devtools"
}
},
"page_action": {
"action": {
"default_icon": "img/logo/38x38.png",
"default_title": "Redux DevTools",
"default_popup": "window.html#popup"
"default_popup": "devpanel.html#popup"
},
"commands": {
"devtools-left": {
"description": "DevTools window to left"
},
"devtools-right": {
"description": "DevTools window to right"
},
"devtools-bottom": {
"description": "DevTools window to bottom"
"devtools-window": {
"description": "DevTools window"
},
"devtools-remote": {
"description": "Remote DevTools"
@ -42,21 +36,22 @@
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.bundle.js", "pagewrap.bundle.js"],
"js": ["content.bundle.js"],
"run_at": "document_start",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["page.bundle.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
}
],
"devtools_page": "devtools.html",
"web_accessible_resources": ["page.bundle.js"],
"permissions": [
"notifications",
"contextMenus",
"tabs",
"storage",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_security_policy": "script-src 'self'; object-src 'self'; img-src 'self' data:;"
"permissions": ["notifications", "contextMenus", "tabs", "storage"],
"host_permissions": ["file:///*", "http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data:;"
}
}

View File

@ -18,7 +18,7 @@ import {
TopButtons,
} from '@redux-devtools/app';
import { GoBroadcast } from 'react-icons/go';
import { MdBorderBottom, MdBorderLeft, MdBorderRight } from 'react-icons/md';
import { MdOutlineWindow } from 'react-icons/md';
import type { Position } from '../pageScript/api/openWindow';
import type { SingleMessage } from '../background/store/apiMiddleware';
@ -98,31 +98,13 @@ class Actions extends Component<Props> {
<DispatcherButton dispatcherIsOpen={this.props.dispatcherIsOpen} />
)}
<Divider />
{!window.isElectron && position !== '#left' && (
{!window.isElectron && (
<Button
onClick={() => {
this.openWindow('left');
this.openWindow('window');
}}
>
<MdBorderLeft />
</Button>
)}
{!window.isElectron && position !== '#right' && (
<Button
onClick={() => {
this.openWindow('right');
}}
>
<MdBorderRight />
</Button>
)}
{!window.isElectron && position !== '#bottom' && (
<Button
onClick={() => {
this.openWindow('bottom');
}}
>
<MdBorderBottom />
<MdOutlineWindow />
</Button>
)}
{!window.isElectron && (

View File

@ -2,29 +2,23 @@ import openDevToolsWindow, { DevToolsPosition } from './openWindow';
export function createMenu() {
const menus = [
{ id: 'devtools-left', title: 'To left' },
{ id: 'devtools-right', title: 'To right' },
{ id: 'devtools-bottom', title: 'To bottom' },
{
id: 'devtools-panel',
title: 'Open in a panel (enable in browser settings)',
},
{ id: 'devtools-window', title: 'Open in a window' },
{ id: 'devtools-remote', title: 'Open Remote DevTools' },
];
let shortcuts: { [commandName: string]: string | undefined } = {};
chrome.commands.getAll((commands) => {
commands.forEach(({ name, shortcut }) => {
for (const { name, shortcut } of commands) {
shortcuts[name!] = shortcut;
});
}
menus.forEach(({ id, title }) => {
for (const { id, title } of menus) {
chrome.contextMenus.create({
id: id,
title: title + (shortcuts[id] ? ' (' + shortcuts[id] + ')' : ''),
contexts: ['all'],
});
});
}
});
}

View File

@ -1,29 +1,22 @@
import '../chromeApiMock';
import { Store } from 'redux';
import configureStore, { BackgroundAction } from './store/backgroundStore';
import configureStore from './store/backgroundStore';
import openDevToolsWindow, { DevToolsPosition } from './openWindow';
import { createMenu, removeMenu } from './contextMenus';
import syncOptions from '../options/syncOptions';
import { BackgroundState } from './store/backgroundReducer';
declare global {
interface Window {
store: Store<BackgroundState, BackgroundAction>;
}
}
import { getOptions } from '../options/syncOptions';
// Expose the extension's store globally to access it from the windows
// via chrome.runtime.getBackgroundPage
window.store = configureStore();
export const store = configureStore();
// Listen for keyboard shortcuts
chrome.commands.onCommand.addListener((shortcut) => {
openDevToolsWindow(shortcut as DevToolsPosition);
});
// Create the context menu when installed
// Disable the action by default and create the context menu when installed
chrome.runtime.onInstalled.addListener(() => {
syncOptions().get((option) => {
chrome.action.disable();
getOptions((option) => {
if (option.showContextMenus) createMenu();
});
});

View File

@ -1,4 +1,5 @@
import { LIFTED_ACTION } from '@redux-devtools/app';
import { store } from './index';
export function getReport(
reportId: string,
@ -24,7 +25,7 @@ export function getReport(
.then((json) => {
const { payload, preloadedState } = json;
if (!payload) return;
window.store.dispatch({
store.dispatch({
type: LIFTED_ACTION,
message: 'IMPORT',
state: JSON.stringify({ payload, preloadedState }),

View File

@ -1,83 +1,34 @@
export type DevToolsPosition =
| 'devtools-left'
| 'devtools-right'
| 'devtools-bottom'
| 'devtools-panel'
| 'devtools-remote';
export type DevToolsPosition = 'devtools-window' | 'devtools-remote';
let windows: { [K in DevToolsPosition]?: number } = {};
let lastPosition: DevToolsPosition | null = null;
export default function openDevToolsWindow(position: DevToolsPosition) {
function popWindow(
action: string,
url: string,
customOptions: chrome.windows.CreateData & chrome.windows.UpdateInfo,
) {
function focusIfExist(callback: () => void) {
if (!windows[position]) {
callback();
lastPosition = position;
} else {
let params = { focused: true };
if (lastPosition !== position && position !== 'devtools-panel') {
params = { ...params, ...customOptions };
}
chrome.windows.update(windows[position]!, params, () => {
lastPosition = null;
if (chrome.runtime.lastError) callback();
});
}
}
focusIfExist(() => {
let options: chrome.windows.CreateData = {
type: 'popup',
...customOptions,
};
if (action === 'open') {
options.url = chrome.extension.getURL(
url + '#' + position.substr(position.indexOf('-') + 1),
);
chrome.windows.create(options, (win) => {
windows[position] = win!.id;
if (navigator.userAgent.indexOf('Firefox') !== -1) {
chrome.windows.update(win!.id!, {
focused: true,
...customOptions,
});
}
});
}
if (!windows[position]) {
createWindow(position);
} else {
chrome.windows.update(windows[position]!, { focused: true }, () => {
if (chrome.runtime.lastError) createWindow(position);
});
}
let params: chrome.windows.CreateData & chrome.windows.UpdateInfo = {
left: 0,
top: 0,
width: 380,
height: window.screen.availHeight,
};
let url = 'window.html';
switch (position) {
case 'devtools-right':
params.left =
(window.screen as unknown as { availLeft: number }).availLeft +
window.screen.availWidth -
params.width!;
break;
case 'devtools-bottom':
params.height = 420;
params.top = window.screen.height - params.height;
params.width = window.screen.availWidth;
break;
case 'devtools-panel':
params.type = 'panel';
break;
case 'devtools-remote':
params = { width: 850, height: 600 };
url = 'remote.html';
break;
}
popWindow('open', url, params);
}
function createWindow(position: DevToolsPosition) {
const url = chrome.runtime.getURL(getPath(position));
chrome.windows.create({ type: 'popup', url }, (win) => {
windows[position] = win!.id;
if (navigator.userAgent.indexOf('Firefox') !== -1) {
chrome.windows.update(win!.id!, { focused: true });
}
});
}
function getPath(position: DevToolsPosition) {
switch (position) {
case 'devtools-window':
return 'devpanel.html';
case 'devtools-remote':
return 'remote.html';
default:
throw new Error(`Unrecognized position: ${position}`);
}
}

View File

@ -11,11 +11,7 @@ import {
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import syncOptions, {
Options,
OptionsMessage,
SyncOptions,
} from '../../options/syncOptions';
import type { Options, OptionsMessage } from '../../options/syncOptions';
import openDevToolsWindow, { DevToolsPosition } from '../openWindow';
import { getReport } from '../logging';
import { Action, Dispatch, Middleware } from 'redux';
@ -32,6 +28,7 @@ import { LiftedState } from '@redux-devtools/instrument';
import type { BackgroundAction, LiftedActionAction } from './backgroundStore';
import type { Position } from '../../pageScript/api/openWindow';
import type { BackgroundState } from './backgroundReducer';
import { store } from '../index';
interface TabMessageBase {
readonly type: string;
@ -51,6 +48,11 @@ interface StopAction extends TabMessageBase {
readonly id?: never;
}
interface OptionsAction {
readonly type: 'OPTIONS';
readonly options: Options;
}
interface DispatchAction extends TabMessageBase {
readonly type: 'DISPATCH';
readonly action: AppDispatchAction;
@ -196,7 +198,7 @@ interface SplitUpdateStateAction<S, A extends Action<string>> {
export type TabMessage =
| StartAction
| StopAction
| OptionsMessage
| OptionsAction
| DispatchAction
| ImportAction
| ActionAction
@ -247,7 +249,6 @@ const chunks: {
>;
} = {};
let monitors = 0;
let isMonitored = false;
const getId = (sender: chrome.runtime.MessageSender, name?: string) =>
sender.tab ? sender.tab.id! : name || sender.id!;
@ -261,22 +262,18 @@ type MonitorAction<S, A extends Action<string>> =
// Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts
const maxChromeMsgSize = 32 * 1024 * 1024;
// TODO Clean up args
function toMonitors<S, A extends Action<string>>(
action: MonitorAction<S, A>,
tabId?: string | number,
verbose?: boolean,
) {
for (const monitorPort of Object.values(connections.monitor)) {
monitorPort.postMessage(
verbose || action.type === 'ERROR' || action.type === SET_PERSIST
? action
: { type: UPDATE_STATE },
);
}
for (const panelPort of Object.values(connections.panel)) {
for (const port of [
...Object.values(connections.monitor),
...Object.values(connections.panel),
]) {
try {
panelPort.postMessage(action);
port.postMessage(action);
} catch (err) {
if (
action.type !== UPDATE_STATE ||
@ -307,11 +304,11 @@ function toMonitors<S, A extends Action<string>>(
value;
}
panelPort.postMessage({ ...action, request: splitMessageStart });
port.postMessage({ ...action, request: splitMessageStart });
for (let i = 0; i < toSplit.length; i++) {
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
panelPort.postMessage({
port.postMessage({
...action,
request: {
split: 'chunk',
@ -324,7 +321,7 @@ function toMonitors<S, A extends Action<string>>(
}
}
panelPort.postMessage({ ...action, request: { split: 'end' } });
port.postMessage({ ...action, request: { split: 'end' } });
}
}
}
@ -346,7 +343,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
window.store,
store,
message,
instanceId,
action as AppDispatchAction,
@ -360,7 +357,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
window.store,
store,
message,
instanceId,
action as unknown as AppDispatchAction,
@ -374,7 +371,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
window.store,
store,
message,
instanceId,
action as unknown as AppDispatchAction,
@ -388,7 +385,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
window.store,
store,
message,
instanceId,
action as unknown as AppDispatchAction,
@ -402,7 +399,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
window.store,
store,
message,
instanceId,
action as AppDispatchAction,
@ -414,14 +411,12 @@ function toContentScript(messageBody: ToContentScriptMessage) {
}
function toAllTabs(msg: TabMessage) {
const tabs = connections.tab;
Object.keys(tabs).forEach((id) => {
tabs[id].postMessage(msg);
});
for (const tabPort of Object.values(connections.tab)) {
tabPort.postMessage(msg);
}
}
function monitorInstances(shouldMonitor: boolean, id?: string) {
if (!id && isMonitored === shouldMonitor) return;
const action = {
type: shouldMonitor ? ('START' as const) : ('STOP' as const),
};
@ -430,11 +425,10 @@ function monitorInstances(shouldMonitor: boolean, id?: string) {
} else {
toAllTabs(action);
}
isMonitored = shouldMonitor;
}
function getReducerError() {
const instancesState = window.store.getState().instances;
const instancesState = store.getState().instances;
const payload = instancesState.states[instancesState.current];
const computedState = payload.computedStates[payload.currentStateIndex];
if (!computedState) return false;
@ -442,11 +436,11 @@ function getReducerError() {
}
function togglePersist() {
const state = window.store.getState();
const state = store.getState();
if (state.instances.persisted) {
Object.keys(state.instances.connections).forEach((id) => {
if (connections.tab[id]) return;
window.store.dispatch({ type: REMOVE_INSTANCE, id });
store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
});
}
@ -461,34 +455,25 @@ interface OpenOptionsMessage {
readonly type: 'OPEN_OPTIONS';
}
interface GetOptionsMessage {
readonly type: 'GET_OPTIONS';
}
export type SingleMessage =
| OpenMessage
| OpenOptionsMessage
| GetOptionsMessage;
export type SingleMessage = OpenMessage | OpenOptionsMessage | OptionsMessage;
type BackgroundStoreMessage<S, A extends Action<string>> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage
| SingleMessage;
type BackgroundStoreResponse = { readonly options: Options };
// Receive messages from content scripts
function messaging<S, A extends Action<string>>(
request: BackgroundStoreMessage<S, A>,
sender: chrome.runtime.MessageSender,
sendResponse?: (response?: BackgroundStoreResponse) => void,
) {
let tabId = getId(sender);
if (!tabId) return;
if (sender.frameId) tabId = `${tabId}-${sender.frameId}`;
if (request.type === 'STOP') {
if (!Object.keys(window.store.getState().instances.connections).length) {
window.store.dispatch({ type: DISCONNECTED });
if (!Object.keys(store.getState().instances.connections).length) {
store.dispatch({ type: DISCONNECTED });
}
return;
}
@ -496,10 +481,8 @@ function messaging<S, A extends Action<string>>(
chrome.runtime.openOptionsPage();
return;
}
if (request.type === 'GET_OPTIONS') {
window.syncOptions.get((options) => {
sendResponse!({ options });
});
if (request.type === 'OPTIONS') {
toAllTabs({ type: 'OPTIONS', options: request.options });
return;
}
if (request.type === 'GET_REPORT') {
@ -507,12 +490,8 @@ function messaging<S, A extends Action<string>>(
return;
}
if (request.type === 'OPEN') {
let position: DevToolsPosition = 'devtools-left';
if (
['remote', 'panel', 'left', 'right', 'bottom'].indexOf(
request.position,
) !== -1
) {
let position: DevToolsPosition = 'devtools-window';
if (['remote', 'window'].includes(request.position)) {
position = ('devtools-' + request.position) as DevToolsPosition;
}
openDevToolsWindow(position);
@ -560,7 +539,7 @@ function messaging<S, A extends Action<string>>(
if (request.instanceId) {
action.request.instanceId = instanceId;
}
window.store.dispatch(action);
store.dispatch(action);
if (request.type === 'EXPORT') {
toMonitors(action, tabId, true);
@ -580,8 +559,8 @@ function disconnect(
if (p) p.onDisconnect.removeListener(disconnectListener);
delete connections[type][id];
if (type === 'tab') {
if (!window.store.getState().instances.persisted) {
window.store.dispatch({ type: REMOVE_INSTANCE, id });
if (!store.getState().instances.persisted) {
store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
} else {
@ -595,21 +574,22 @@ function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
let id: number | string;
let listener;
window.store.dispatch({ type: CONNECTED, port });
store.dispatch({ type: CONNECTED, port });
if (port.name === 'tab') {
id = getId(port.sender!);
if (port.sender!.frameId) id = `${id}-${port.sender!.frameId}`;
connections.tab[id] = port;
listener = (msg: ContentScriptToBackgroundMessage<S, A>) => {
listener = (msg: ContentScriptToBackgroundMessage<S, A> | 'heartbeat') => {
if (msg === 'heartbeat') return;
if (msg.name === 'INIT_INSTANCE') {
if (typeof id === 'number') {
chrome.pageAction.show(id);
chrome.pageAction.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
chrome.action.enable(id);
chrome.action.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
}
if (isMonitored) port.postMessage({ type: 'START' });
port.postMessage({ type: 'START' });
const state = window.store.getState();
const state = store.getState();
if (state.instances.persisted) {
const instanceId = `${id}/${msg.instanceId}`;
const persistedState = state.instances.states[instanceId];
@ -636,6 +616,11 @@ function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
id = getId(port.sender!, port.name);
connections.monitor[id] = port;
monitorInstances(true);
listener = (msg: BackgroundAction | 'heartbeat') => {
if (msg === 'heartbeat') return;
store.dispatch(msg);
};
port.onMessage.addListener(listener);
monitors++;
port.onDisconnect.addListener(disconnect('monitor', id));
} else {
@ -644,8 +629,9 @@ function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
connections.panel[id] = port;
monitorInstances(true, port.name);
monitors++;
listener = (msg: BackgroundAction) => {
window.store.dispatch(msg);
listener = (msg: BackgroundAction | 'heartbeat') => {
if (msg === 'heartbeat') return;
store.dispatch(msg);
};
port.onMessage.addListener(listener);
port.onDisconnect.addListener(disconnect('panel', id, listener));
@ -659,17 +645,9 @@ chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener((id) => {
chrome.notifications.clear(id);
openDevToolsWindow('devtools-right');
openDevToolsWindow('devtools-window');
});
declare global {
interface Window {
syncOptions: SyncOptions;
}
}
window.syncOptions = syncOptions(toAllTabs); // Expose to the options page
const api: Middleware<{}, BackgroundState, Dispatch<BackgroundAction>> =
(store) => (next) => (untypedAction) => {
const action = untypedAction as BackgroundAction;

View File

@ -1,8 +1,10 @@
import '../chromeApiMock';
import {
injectOptions,
getOptionsFromBg,
getOptions,
isAllowed,
Options,
prefetchOptions,
prepareOptionsForPage,
} from '../options/syncOptions';
import type { TabMessage } from '../background/store/apiMiddleware';
import type {
@ -84,6 +86,13 @@ interface UpdateAction {
readonly source: typeof source;
}
interface OptionsAction {
readonly type: 'OPTIONS';
readonly options: Options;
readonly id: undefined;
readonly source: typeof source;
}
export type ContentScriptToPageScriptMessage =
| StartAction
| StopAction
@ -91,7 +100,8 @@ export type ContentScriptToPageScriptMessage =
| ImportAction
| ActionAction
| ExportAction
| UpdateAction;
| UpdateAction
| OptionsAction;
interface ImportStatePayload<S, A extends Action<string>> {
readonly type: 'IMPORT_STATE';
@ -112,6 +122,7 @@ export type ListenerMessage<S, A extends Action<string>> =
| ActionAction
| ExportAction
| UpdateAction
| OptionsAction
| ImportStateDispatchAction<S, A>;
function postToPageScript(message: ContentScriptToPageScriptMessage) {
@ -156,8 +167,13 @@ function connect() {
source,
});
}
} else if ('options' in message) {
injectOptions(message.options);
} else if (message.type === 'OPTIONS') {
postToPageScript({
type: message.type,
options: prepareOptionsForPage(message.options),
id: undefined,
source,
});
} else {
postToPageScript({
type: message.type,
@ -289,7 +305,14 @@ function send<S, A extends Action<string>>(
) {
if (!connected) connect();
if (message.type === 'INIT_INSTANCE') {
getOptionsFromBg();
getOptions((options) => {
postToPageScript({
type: 'OPTIONS',
options: prepareOptionsForPage(options),
id: undefined,
source,
});
});
postToBackground({ name: 'INIT_INSTANCE', instanceId: message.instanceId });
} else {
postToBackground({ name: 'RELAY', message });
@ -317,4 +340,10 @@ function handleMessages<S, A extends Action<string>>(
tryCatch(send, message);
}
prefetchOptions();
window.addEventListener('message', handleMessages, false);
setInterval(() => {
bg?.postMessage('heartbeat');
}, 15000);

View File

@ -5,12 +5,13 @@ html
meta(charset='UTF-8')
title Redux DevTools
include ../style.pug
style.
body {
min-height: 100px;
}
body
#root
div(style='display: flex; justify-content: center; align-items: center')
img(
src='/img/loading.svg',
height=300, width=350,
)
link(href='/devpanel.bundle.css', rel='stylesheet')
script(src='/devpanel.bundle.js')

View File

@ -6,6 +6,7 @@ import { Persistor } from 'redux-persist';
import {
REMOVE_INSTANCE,
StoreAction,
StoreState,
UPDATE_STATE,
} from '@redux-devtools/app';
import App from '../app/App';
@ -18,19 +19,19 @@ import {
SplitUpdateStateRequest,
UpdateStateRequest,
} from '../background/store/apiMiddleware';
import type { StoreStateWithoutSocket } from './store/panelReducer';
import { PersistGate } from 'redux-persist/integration/react';
const position = location.hash;
const messageStyle: CSSProperties = {
padding: '20px',
paddingTop: '20px',
width: '100%',
textAlign: 'center',
boxSizing: 'border-box',
};
let rendered: boolean | undefined;
let currentRoot: Root | undefined;
let store: Store<StoreStateWithoutSocket, StoreAction> | undefined;
let store: Store<StoreState, StoreAction> | undefined;
let persistor: Persistor | undefined;
let bgConnection: chrome.runtime.Port;
let naTimeout: NodeJS.Timeout;
@ -72,7 +73,12 @@ function renderNA() {
.
</div>
);
if (isChrome) {
if (
isChrome &&
chrome &&
chrome.devtools &&
chrome.devtools.inspectedWindow
) {
chrome.devtools.inspectedWindow.getResources((resources) => {
if (resources[0].url.substr(0, 4) === 'file') {
message = (
@ -101,17 +107,26 @@ function renderNA() {
let splitMessage: SplitUpdateStateRequest<unknown, Action<string>>;
function init(id: number) {
function init() {
renderNA();
bgConnection = chrome.runtime.connect({
name: id ? id.toString() : undefined,
});
let name = 'monitor';
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
name += chrome.devtools.inspectedWindow.tabId;
}
bgConnection = chrome.runtime.connect({ name });
setInterval(() => {
bgConnection.postMessage('heartbeat');
}, 15000);
bgConnection.onMessage.addListener(
<S, A extends Action<string>>(
message: PanelMessageWithSplitAction<S, A>,
) => {
if (message.type === 'NA') {
if (message.id === id) renderNA();
// TODO Double-check this now that the name is different
if (message.id === name) renderNA();
else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id });
} else {
if (!rendered) renderDevTools();
@ -157,4 +172,7 @@ function init(id: number) {
);
}
init(chrome.devtools.inspectedWindow.tabId);
if (position === '#popup') document.body.style.minWidth = '760px';
if (position !== '#popup') document.body.style.minHeight = '100%';
init();

View File

@ -1,45 +1,29 @@
import { combineReducers, Reducer } from 'redux';
import {
connection,
ConnectionState,
instances,
InstancesState,
monitor,
MonitorState,
notification,
NotificationState,
reports,
ReportsState,
section,
SectionState,
StateTreeSettings,
socket,
stateTreeSettings,
StoreAction,
StoreState,
theme,
ThemeState,
} from '@redux-devtools/app';
export interface StoreStateWithoutSocket {
readonly section: SectionState;
readonly theme: ThemeState;
readonly connection: ConnectionState;
readonly monitor: MonitorState;
readonly instances: InstancesState;
readonly reports: ReportsState;
readonly notification: NotificationState;
readonly stateTreeSettings: StateTreeSettings;
}
const rootReducer: Reducer<
StoreStateWithoutSocket,
StoreState,
StoreAction,
Partial<StoreStateWithoutSocket>
Partial<StoreState>
> = combineReducers({
instances,
monitor,
reports,
notification,
section,
socket,
theme,
connection,
stateTreeSettings,

View File

@ -1,9 +1,13 @@
import { createStore, applyMiddleware, Reducer, Store } from 'redux';
import localForage from 'localforage';
import { persistReducer, persistStore } from 'redux-persist';
import { exportStateMiddleware, StoreAction } from '@redux-devtools/app';
import {
exportStateMiddleware,
StoreAction,
StoreState,
} from '@redux-devtools/app';
import panelDispatcher from './panelSyncMiddleware';
import rootReducer, { StoreStateWithoutSocket } from './panelReducer';
import rootReducer from './panelReducer';
const persistConfig = {
key: 'redux-devtools',
@ -11,8 +15,10 @@ const persistConfig = {
storage: localForage,
};
const persistedReducer: Reducer<StoreStateWithoutSocket, StoreAction> =
persistReducer(persistConfig, rootReducer) as any;
const persistedReducer: Reducer<StoreState, StoreAction> = persistReducer(
persistConfig,
rootReducer,
) as any;
export default function configureStore(
position: string,

View File

@ -7,23 +7,51 @@ import {
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import { Dispatch, Middleware } from 'redux';
import { Dispatch, Middleware, MiddlewareAPI } from 'redux';
function selectInstance(
tabId: number,
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
next: (action: unknown) => unknown,
) {
const instances = store.getState().instances;
if (instances.current === 'default') return;
const connections = instances.connections[tabId];
if (connections && connections.length === 1) {
next({ type: SELECT_INSTANCE, selected: connections[0] });
}
}
function getCurrentTabId(next: (tabId: number) => void) {
chrome.tabs.query(
{
active: true,
lastFocusedWindow: true,
},
(tabs) => {
const tab = tabs[0];
if (!tab) return;
next(tab.id!);
},
);
}
function panelDispatcher(
bgConnection: chrome.runtime.Port,
): Middleware<{}, StoreState, Dispatch<StoreAction>> {
let autoselected = false;
const tabId = chrome.devtools.inspectedWindow.tabId;
return (store) => (next) => (untypedAction) => {
const action = untypedAction as StoreAction;
const result = next(action);
if (!autoselected && action.type === UPDATE_STATE && tabId) {
if (!autoselected && action.type === UPDATE_STATE) {
autoselected = true;
const connections = store.getState().instances.connections[tabId];
if (connections && connections.length === 1) {
next({ type: SELECT_INSTANCE, selected: connections[0] });
if (chrome.devtools && chrome.devtools.inspectedWindow) {
selectInstance(chrome.devtools.inspectedWindow.tabId, store, next);
} else {
getCurrentTabId((tabId) => selectInstance(tabId, store, next));
}
}
if (action.type === LIFTED_ACTION || action.type === TOGGLE_PERSIST) {

View File

@ -1,17 +1,6 @@
function createPanel(url: string) {
chrome.devtools.panels.create(
'Redux',
'img/logo/scalable.png',
url,
function () {},
);
}
if (chrome.runtime.getBackgroundPage) {
// Check if the background page's object is accessible (not in incognito)
chrome.runtime.getBackgroundPage((background) => {
createPanel(background ? 'window.html' : 'devpanel.html');
});
} else {
createPanel('devpanel.html');
}
chrome.devtools.panels.create(
'Redux',
'img/logo/scalable.png',
'devpanel.html',
() => {},
);

View File

@ -2,22 +2,25 @@ import '../chromeApiMock';
import React from 'react';
import { createRoot } from 'react-dom/client';
import OptionsComponent from './Options';
import { Options } from './syncOptions';
import {
getOptions,
Options,
OptionsMessage,
saveOption,
subscribeToOptions,
} from './syncOptions';
chrome.runtime.getBackgroundPage((background) => {
const syncOptions = background!.syncOptions;
const saveOption = <K extends keyof Options>(name: K, value: Options[K]) => {
syncOptions.save(name, value);
};
const renderOptions = (options: Options) => {
const root = createRoot(document.getElementById('root')!);
root.render(<OptionsComponent options={options} saveOption={saveOption} />);
};
syncOptions.subscribe(renderOptions);
syncOptions.get((options) => {
renderOptions(options);
});
subscribeToOptions((options) => {
const message: OptionsMessage = { type: 'OPTIONS', options };
chrome.runtime.sendMessage(message);
});
const renderOptions = (options: Options) => {
const root = createRoot(document.getElementById('root')!);
root.render(<OptionsComponent options={options} saveOption={saveOption} />);
};
subscribeToOptions(renderOptions);
getOptions((options) => {
renderOptions(options);
});

View File

@ -38,21 +38,22 @@ let options: Options | undefined;
let subscribers: ((options: Options) => void)[] = [];
export interface OptionsMessage {
readonly type: 'OPTIONS';
readonly options: Options;
}
type ToAllTabs = (msg: OptionsMessage) => void;
const save =
(toAllTabs: ToAllTabs | undefined) =>
<K extends keyof Options>(key: K, value: Options[K]) => {
let obj: { [K1 in keyof Options]?: Options[K1] } = {};
obj[key] = value;
chrome.storage.sync.set(obj);
options![key] = value;
toAllTabs!({ options: options! });
subscribers.forEach((s) => s(options!));
};
export const saveOption = <K extends keyof Options>(
key: K,
value: Options[K],
) => {
let obj: { [K1 in keyof Options]?: Options[K1] } = {};
obj[key] = value;
chrome.storage.sync.set(obj);
options![key] = value;
for (const subscriber of subscribers) {
subscriber(options!);
}
};
const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({
...oldOptions,
@ -71,7 +72,7 @@ const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({
: oldOptions.filter,
});
const get = (callback: (options: Options) => void) => {
export const getOptions = (callback: (options: Options) => void) => {
if (options) callback(options);
else {
chrome.storage.sync.get(
@ -98,67 +99,29 @@ const get = (callback: (options: Options) => void) => {
}
};
const subscribe = (callback: (options: Options) => void) => {
export const prefetchOptions = () => getOptions(() => {});
export const subscribeToOptions = (callback: (options: Options) => void) => {
subscribers = subscribers.concat(callback);
};
const toReg = (str: string) =>
str !== '' ? str.split('\n').filter(Boolean).join('|') : null;
export const injectOptions = (newOptions: Options) => {
if (!newOptions) return;
options = {
...newOptions,
allowlist:
newOptions.filter !== FilterState.DO_NOT_FILTER
? toReg(newOptions.allowlist)!
: newOptions.allowlist,
denylist:
newOptions.filter !== FilterState.DO_NOT_FILTER
? toReg(newOptions.denylist)!
: newOptions.denylist,
};
let s = document.createElement('script');
s.type = 'text/javascript';
s.appendChild(
document.createTextNode(
'window.devToolsOptions = Object.assign(window.devToolsOptions||{},' +
JSON.stringify(options) +
');',
),
);
(document.head || document.documentElement).appendChild(s);
s.parentNode!.removeChild(s);
};
export const getOptionsFromBg = () => {
/* chrome.runtime.sendMessage({ type: 'GET_OPTIONS' }, response => {
if (response && response.options) injectOptions(response.options);
});
*/
get((newOptions) => {
injectOptions(newOptions);
}); // Legacy
};
export const prepareOptionsForPage = (options: Options): Options => ({
...options,
allowlist:
options.filter !== FilterState.DO_NOT_FILTER
? toReg(options.allowlist)!
: options.allowlist,
denylist:
options.filter !== FilterState.DO_NOT_FILTER
? toReg(options.denylist)!
: options.denylist,
});
export const isAllowed = (localOptions = options) =>
!localOptions ||
localOptions.inject ||
!localOptions.urls ||
location.href.match(toReg(localOptions.urls)!);
export interface SyncOptions {
readonly save: <K extends keyof Options>(key: K, value: Options[K]) => void;
readonly get: (callback: (options: Options) => void) => void;
readonly subscribe: (callback: (options: Options) => void) => void;
}
export default function syncOptions(toAllTabs?: ToAllTabs): SyncOptions {
if (toAllTabs && !options) get(() => {}); // Initialize
return {
save: save(toAllTabs),
get: get,
subscribe: subscribe,
};
}

View File

@ -1,7 +1,7 @@
import { Action } from 'redux';
import type { PageScriptToContentScriptMessage } from './index';
export type Position = 'left' | 'right' | 'bottom' | 'panel' | 'remote';
export type Position = 'window' | 'remote';
function post<S, A extends Action<string>>(
message: PageScriptToContentScriptMessage<S, A>,
@ -13,6 +13,6 @@ export default function openWindow(position?: Position) {
post({
source: '@devtools-page',
type: 'OPEN',
position: position || 'right',
position: position ?? 'window',
});
}

View File

@ -432,6 +432,13 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
serializeAction,
);
}
return;
case 'OPTIONS':
window.devToolsOptions = Object.assign(
window.devToolsOptions || {},
message.options,
);
return;
}
}

View File

@ -1,19 +0,0 @@
// @ts-ignore
import script from '../dist/page.bundle.js';
let s = document.createElement('script');
s.type = 'text/javascript';
if (process.env.NODE_ENV === 'production') {
s.appendChild(document.createTextNode(script));
(document.head || document.documentElement).appendChild(s);
s.parentNode!.removeChild(s);
} else {
s.src = chrome.extension.getURL('page.bundle.js');
s.onload = function () {
(this as HTMLScriptElement).parentNode!.removeChild(
this as HTMLScriptElement,
);
};
(document.head || document.documentElement).appendChild(s);
}

View File

@ -1,37 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { UPDATE_STATE } from '@redux-devtools/app';
import App from '../app/App';
import configureStore from './store/windowStore';
import type { MonitorMessage } from '../background/store/apiMiddleware';
const position = location.hash;
chrome.runtime.getBackgroundPage((window) => {
const { store } = window!;
const { store: localStore, persistor } = configureStore(store, position);
let name = 'monitor';
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
name += chrome.devtools.inspectedWindow.tabId;
}
const bg = chrome.runtime.connect({ name });
const update = (action?: MonitorMessage) => {
localStore.dispatch(action || { type: UPDATE_STATE });
};
bg.onMessage.addListener(update);
update();
const root = createRoot(document.getElementById('root')!);
root.render(
<Provider store={localStore}>
<PersistGate loading={null} persistor={persistor}>
<App position={position} />
</PersistGate>
</Provider>,
);
});
if (position === '#popup') document.body.style.minWidth = '760px';
if (position !== '#popup') document.body.style.minHeight = '100%';

View File

@ -1,51 +0,0 @@
import { Dispatch, Middleware, MiddlewareAPI } from 'redux';
import {
SELECT_INSTANCE,
StoreAction,
StoreState,
UPDATE_STATE,
} from '@redux-devtools/app';
function selectInstance(
tabId: number,
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
next: (action: unknown) => unknown,
) {
const instances = store.getState().instances;
if (instances.current === 'default') return;
const connections = instances.connections[tabId];
if (connections && connections.length === 1) {
next({ type: SELECT_INSTANCE, selected: connections[0] });
}
}
function getCurrentTabId(next: (tabId: number) => void) {
chrome.tabs.query(
{
active: true,
lastFocusedWindow: true,
},
(tabs) => {
const tab = tabs[0];
if (!tab) return;
next(tab.id!);
},
);
}
const popupSelector: Middleware<{}, StoreState, Dispatch<StoreAction>> =
(store) => (next) => (untypedAction) => {
const action = untypedAction as StoreAction;
const result = next(action);
if (action.type === UPDATE_STATE) {
if (chrome.devtools && chrome.devtools.inspectedWindow) {
selectInstance(chrome.devtools.inspectedWindow.tabId, store, next);
} else {
getCurrentTabId((tabId) => selectInstance(tabId, store, next));
}
}
return result;
};
export default popupSelector;

View File

@ -1,34 +0,0 @@
import {
instancesInitialState,
dispatchAction,
UPDATE_STATE,
SELECT_INSTANCE,
LIFTED_ACTION,
SET_PERSIST,
} from '@redux-devtools/app';
import type {
ExpandedUpdateStateAction,
WindowStoreAction,
} from './windowStore';
export default function instances(
state = instancesInitialState,
action: WindowStoreAction,
) {
switch (action.type) {
case UPDATE_STATE:
return {
...(action as ExpandedUpdateStateAction).instances,
selected: state.selected,
};
case LIFTED_ACTION:
if (action.message === 'DISPATCH') return dispatchAction(state, action);
return state;
case SELECT_INSTANCE:
return { ...state, selected: action.selected };
case SET_PERSIST:
return { ...state, persisted: action.payload };
default:
return state;
}
}

View File

@ -1,32 +0,0 @@
import { combineReducers, Reducer } from 'redux';
import {
connection,
monitor,
notification,
reports,
section,
socket,
theme,
stateTreeSettings,
StoreState,
} from '@redux-devtools/app';
import instances from './instancesReducer';
import type { WindowStoreAction } from './windowStore';
const rootReducer: Reducer<
StoreState,
WindowStoreAction,
Partial<StoreState>
> = combineReducers({
instances,
monitor,
socket,
reports,
notification,
section,
theme,
connection,
stateTreeSettings,
}) as any;
export default rootReducer;

View File

@ -1,81 +0,0 @@
import {
createStore,
compose,
applyMiddleware,
Store,
StoreEnhancer,
Reducer,
} from 'redux';
import localForage from 'localforage';
import { persistReducer, persistStore } from 'redux-persist';
import {
api,
CONNECT_REQUEST,
exportStateMiddleware,
InstancesState,
StoreActionWithoutUpdateState,
StoreState,
UpdateStateAction,
} from '@redux-devtools/app';
import syncStores from './windowSyncMiddleware';
import instanceSelector from './instanceSelectorMiddleware';
import rootReducer from './windowReducer';
import type { BackgroundState } from '../../background/store/backgroundReducer';
import type { BackgroundAction } from '../../background/store/backgroundStore';
import type {
EmptyUpdateStateAction,
NAAction,
} from '../../background/store/apiMiddleware';
export interface ExpandedUpdateStateAction extends UpdateStateAction {
readonly instances: InstancesState;
}
export type WindowStoreAction =
| StoreActionWithoutUpdateState
| ExpandedUpdateStateAction
| NAAction
| EmptyUpdateStateAction;
const persistConfig = {
key: 'redux-devtools',
blacklist: ['instances', 'socket'],
storage: localForage,
};
const persistedReducer: Reducer<StoreState, WindowStoreAction> = persistReducer(
persistConfig,
rootReducer,
) as any;
export default function configureStore(
baseStore: Store<BackgroundState, BackgroundAction>,
position: string,
) {
let enhancer: StoreEnhancer;
const middlewares = [exportStateMiddleware, api, syncStores(baseStore)];
if (!position || position === '#popup') {
// select current tab instance for devPanel and pageAction
middlewares.push(instanceSelector);
}
if (process.env.NODE_ENV === 'production') {
enhancer = applyMiddleware(...middlewares);
} else {
enhancer = compose(
applyMiddleware(...middlewares),
window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (noop: unknown) => noop,
);
}
const store = createStore(persistedReducer, enhancer);
const persistor = persistStore(store as Store, null, () => {
if (store.getState().connection.type !== 'disabled') {
store.dispatch({
type: CONNECT_REQUEST,
});
}
});
return { store, persistor };
}

View File

@ -1,37 +0,0 @@
import {
getActiveInstance,
LIFTED_ACTION,
StoreAction,
StoreState,
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import { Dispatch, Middleware, Store } from 'redux';
import type { BackgroundState } from '../../background/store/backgroundReducer';
import type { BackgroundAction } from '../../background/store/backgroundStore';
const syncStores =
(
baseStore: Store<BackgroundState, BackgroundAction>,
): Middleware<{}, StoreState, Dispatch<StoreAction>> =>
(store) =>
(next) =>
(untypedAction) => {
const action = untypedAction as StoreAction;
if (action.type === UPDATE_STATE) {
return next({
...action,
instances: baseStore.getState().instances,
});
}
if (action.type === LIFTED_ACTION || action.type === TOGGLE_PERSIST) {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
baseStore.dispatch({ ...action, instanceId, id } as any);
}
return next(action);
};
export default syncStores;

View File

@ -1,18 +0,0 @@
doctype html
html
head
meta(charset='UTF-8')
title Redux DevTools
include ../style.pug
body
#root
div(style='position: relative')
img(
src='/img/loading.svg',
height=300, width=350,
style='position: absolute; top: 50%; left: 50%; margin-top: -175px; margin-left: -175px;'
)
link(href='/window.bundle.css', rel='stylesheet')
script(src='/window.bundle.js')

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from '../../../src/window/store/windowStore';
import configureStore from '../../../src/devpanel/store/panelStore';
import App from '../../../src/app/App';
Object.defineProperty(window, 'matchMedia', {

View File

@ -20,16 +20,7 @@ describe('API', () => {
expect(message).toEqual({
source: '@devtools-page',
type: 'OPEN',
position: 'right',
});
message = await listenMessage(() => {
window.__REDUX_DEVTOOLS_EXTENSION__.open('left');
});
expect(message).toEqual({
source: '@devtools-page',
type: 'OPEN',
position: 'left',
position: 'window',
});
});

View File

@ -27,9 +27,9 @@ describe('Chrome extension', function () {
});
it("should open extension's window", async () => {
await driver.get(`chrome-extension://${extensionId}/window.html#left`);
await driver.get(`chrome-extension://${extensionId}/devpanel.html`);
const url = await driver.getCurrentUrl();
expect(url).toBe(`chrome-extension://${extensionId}/window.html#left`);
expect(url).toBe(`chrome-extension://${extensionId}/devpanel.html`);
});
it('should match document title', async () => {
@ -37,25 +37,6 @@ describe('Chrome extension', function () {
expect(title).toBe('Redux DevTools');
});
it("should contain inspector monitor's component", async () => {
await delay(1000);
const val = await driver
.findElement(webdriver.By.xpath('//div[@data-testid="inspector"]'))
.getText();
expect(val).toBeDefined();
});
it('should contain an empty actions list', async () => {
const val = await driver
.findElement(webdriver.By.xpath('//div[@data-testid="actionListRows"]'))
.getText();
expect(val).toBe('');
});
Object.keys(switchMonitorTests).forEach((description) =>
it(description, () => switchMonitorTests[description](driver)),
);
it('should get actions list', async () => {
const url = 'https://zalmoxisus.github.io/examples/router/';
await driver.executeScript(`window.open('${url}')`);
@ -68,6 +49,7 @@ describe('Chrome extension', function () {
await driver.switchTo().window(tabs[0]);
await delay(1000);
const result = await driver.wait(
driver
.findElement(webdriver.By.xpath('//div[@data-testid="actionListRows"]'))
@ -80,4 +62,15 @@ describe('Chrome extension', function () {
);
expect(result).toBeTruthy();
});
it("should contain inspector monitor's component", async () => {
const val = await driver
.findElement(webdriver.By.xpath('//div[@data-testid="inspector"]'))
.getText();
expect(val).toBeDefined();
});
Object.keys(switchMonitorTests).forEach((description) =>
it(description, () => switchMonitorTests[description](driver)),
);
});

View File

@ -76,7 +76,7 @@ describe('DevTools panel for Electron', function () {
expect(className).not.toMatch(/hidden/); // not hidden
});
it('should have Redux DevTools UI on current tab', async () => {
it.skip('should have Redux DevTools UI on current tab', async () => {
await driver
.switchTo()
.frame(
@ -87,7 +87,7 @@ describe('DevTools panel for Electron', function () {
await delay(1000);
});
it('should contain INIT action', async () => {
it.skip('should contain INIT action', async () => {
const element = await driver.wait(
webdriver.until.elementLocated(
webdriver.By.xpath('//div[@data-testid="actionListRows"]'),
@ -99,7 +99,7 @@ describe('DevTools panel for Electron', function () {
expect(val).toMatch(/@@INIT/);
});
it("should contain Inspector monitor's component", async () => {
it.skip("should contain Inspector monitor's component", async () => {
const val = await driver
.findElement(webdriver.By.xpath('//div[@data-testid="inspector"]'))
.getText();
@ -107,7 +107,7 @@ describe('DevTools panel for Electron', function () {
});
Object.keys(switchMonitorTests).forEach((description) =>
it(description, () => switchMonitorTests[description](driver)),
it.skip(description, () => switchMonitorTests[description](driver)),
);
/* it('should be no logs in console of main window', async () => {