Skip to content

Commit

Permalink
Merge branch 'main' into bhall/support-choices
Browse files Browse the repository at this point in the history
  • Loading branch information
brianhall authored Nov 21, 2024
2 parents 3ff4761 + 22e3eb0 commit 7876fb9
Show file tree
Hide file tree
Showing 27 changed files with 569 additions and 68 deletions.
3 changes: 3 additions & 0 deletions messaging/lib/messaging.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ interface UnstableMockCall {

interface Window {
webkit: UnstableWebkit;
windowsInteropPostMessage: Window['postMessage'];
windowsInteropAddEventListener: Window['addEventListener'];
windowsInteropRemoveEventListener: Window['removeEventListener'];
__playwright_01: {
mockResponses: Record<string, import('../index.js').MessageResponse>;
subscriptionEvents: import('../index.js').SubscriptionEvent[];
Expand Down
96 changes: 96 additions & 0 deletions messaging/lib/test-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,102 @@ export function mockWindowsMessaging(params) {
* messageCallback: string,
* }} params
*/
export function mockWindowsInteropMessaging(params) {
if (!window.__playwright_01) {
window.__playwright_01 = {
mockResponses: params.responses,
subscriptionEvents: [],
mocks: {
outgoing: [],
},
};
}
const listeners = [];
/**
* @param {AnyWindowsMessage} input
*/
window.windowsInteropPostMessage = (input) => {
// subscription events come through here also
if ('subscriptionName' in input) {
setTimeout(() => {
for (const listener of listeners) {
listener({ origin: window.origin, data: input });
}
}, 0);
return;
}
/** @type {NotificationMessage | RequestMessage} */
let msg = {
context: input.Feature,
featureName: input.SubFeatureName,
params: input.Data,
method: input.Name,
id: undefined,
};

// add the Id if it was a RequestMessage
if ('Id' in input) {
msg = {
...msg,
id: input.Id,
};
}

// record the call
window.__playwright_01.mocks.outgoing.push(
JSON.parse(
JSON.stringify({
payload: msg,
}),
),
);

// if there's no 'id' field, we don't need to respond
if (!('id' in msg)) return;

// If we get here, it needed a response **and** we have a value for it
setTimeout(() => {
// if the mocked response is absent, bail with an error
if (!(msg.method in window.__playwright_01.mockResponses)) {
throw new Error('response not found for ' + msg.method);
}

// now access the response
const response = window.__playwright_01.mockResponses[msg.method];

for (const listener of listeners) {
listener({
origin: window.origin,
/** @type {Omit<MessageResponse, 'error'>} */
data: {
result: response,
context: msg.context,
featureName: msg.featureName,
id: msg.id,
},
});
}
}, 0);
};
window.windowsInteropRemoveEventListener = (_name, _listener) => {
const index = listeners.indexOf(_listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
window.windowsInteropAddEventListener = (_name, listener) => {
listeners.push(listener);
};
}

/**
* Install a mock interface for windows messaging
* @param {{
* messagingContext: import('../index.js').MessagingContext,
* responses: Record<string, any>,
* messageCallback: string,
* }} params
*/
export function mockWebkitMessaging(params) {
if (!window.__playwright_01) {
window.__playwright_01 = {
Expand Down
5 changes: 5 additions & 0 deletions special-pages/messages/new-tab/favorites_move.notify.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"type": "object",
"required": [
"id",
"fromIndex",
"targetIndex"
],
"properties": {
Expand All @@ -14,6 +15,10 @@
"targetIndex": {
"description": "zero-indexed target",
"type": "number"
},
"fromIndex": {
"description": "zero-indexed source",
"type": "number"
}
}
}
7 changes: 6 additions & 1 deletion special-pages/messages/new-tab/favorites_open.notify.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
"type": "object",
"required": [
"id",
"target"
"target",
"url"
],
"properties": {
"id": {
"description": "Entity ID",
"type": "string"
},
"url": {
"description": "The url to open",
"type": "string"
},
"target": {
"type": "string",
"enum": ["same-tab", "new-tab", "new-window"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const FavoritesMemo = memo(Favorites);
* @param {Expansion} props.expansion
* @param {() => void} props.toggle
* @param {(id: string) => void} props.openContextMenu
* @param {(id: string, target: OpenTarget) => void} props.openFavorite
* @param {(id: string, url: string, target: OpenTarget) => void} props.openFavorite
* @param {() => void} props.add
*/
export function Favorites({ gridRef, favorites, expansion, toggle, openContextMenu, openFavorite, add }) {
Expand Down Expand Up @@ -89,16 +89,16 @@ export function Favorites({ gridRef, favorites, expansion, toggle, openContextMe
function onClick(event) {
let target = /** @type {HTMLElement|null} */ (event.target);
while (target && target !== event.currentTarget) {
if (typeof target.dataset.id === 'string') {
if (typeof target.dataset.id === 'string' && 'href' in target && typeof target.href === 'string') {
event.preventDefault();
event.stopImmediatePropagation();
const isControlClick = platformName === 'macos' ? event.metaKey : event.ctrlKey;
if (isControlClick) {
return openFavorite(target.dataset.id, 'new-tab');
return openFavorite(target.dataset.id, target.href, 'new-tab');
} else if (event.shiftKey) {
return openFavorite(target.dataset.id, 'new-window');
return openFavorite(target.dataset.id, target.href, 'new-window');
}
return openFavorite(target.dataset.id, 'same-tab');
return openFavorite(target.dataset.id, target.href, 'same-tab');
} else {
target = target.parentElement;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { h } from 'preact';
import { useContext } from 'preact/hooks';

import { useTypedTranslation } from '../../types.js';
import { useTelemetry, useTypedTranslation } from '../../types.js';
import { useVisibility } from '../../widget-list/widget-config.provider.js';
import { useCustomizer } from '../../customizer/Customizer.js';

Expand All @@ -14,8 +14,10 @@ import { FavoritesMemo } from './Favorites.js';
*/
export function FavoritesConsumer() {
const { state, toggle, favoritesDidReOrder, openContextMenu, openFavorite, add } = useContext(FavoritesContext);
const telemetry = useTelemetry();

if (state.status === 'ready') {
telemetry.measureFromPageLoad('favorites-will-render', 'time to favorites');
return (
<PragmaticDND items={state.data.favorites} itemsDidReOrder={favoritesDidReOrder}>
<FavoritesMemo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { reducer, useConfigSubscription, useDataSubscription, useInitialDataAndC
* @typedef {import('../../../../../types/new-tab.ts').FavoritesOpenAction['target']} OpenTarget
* @typedef {import('../../service.hooks.js').State<FavoritesData, FavoritesConfig>} State
* @typedef {import('../../service.hooks.js').Events<FavoritesData, FavoritesConfig>} Events
* @typedef {{id: string; url: string}} BaseFavoriteType
*/

/**
* @template {BaseFavoriteType} ItemType - allow any type that extends BaseFavoriteType
* @typedef {(params: { list: ItemType[], id: string, fromIndex: number, targetIndex: number }) => void} ReorderFn
*/

/**
Expand All @@ -24,15 +30,15 @@ export const FavoritesContext = createContext({
toggle: () => {
throw new Error('must implement');
},
/** @type {(list: Favorite[], id: string, targetIndex: number) => void} */
favoritesDidReOrder: (list, id, targetIndex) => {
/** @type {ReorderFn<Favorite>} */
favoritesDidReOrder: ({ list, id, fromIndex, targetIndex }) => {
throw new Error('must implement');
},
/** @type {(id: string) => void} */
openContextMenu: (id) => {
throw new Error('must implement');
},
/** @type {(id: string, target: OpenTarget) => void} */
/** @type {(id: string, url: string, target: OpenTarget) => void} */
openFavorite: (id, target) => {
throw new Error('must implement');
},
Expand Down Expand Up @@ -68,11 +74,11 @@ export function FavoritesProvider({ children }) {
// subscribe to toggle + expose a fn for sync toggling
const { toggle } = useConfigSubscription({ dispatch, service });

/** @type {(f: Favorite[], id: string, targetIndex: number) => void} */
/** @type {ReorderFn<Favorite>} */
const favoritesDidReOrder = useCallback(
(favorites, id, targetIndex) => {
({ list, id, fromIndex, targetIndex }) => {
if (!service.current) return;
service.current.setFavoritesOrder({ favorites }, id, targetIndex);
service.current.setFavoritesOrder({ favorites: list }, id, fromIndex, targetIndex);
},
[service],
);
Expand All @@ -86,11 +92,11 @@ export function FavoritesProvider({ children }) {
[service],
);

/** @type {(id: string, target: OpenTarget) => void} */
/** @type {(id: string, url: string, target: OpenTarget) => void} */
const openFavorite = useCallback(
(id, target) => {
(id, url, target) => {
if (!service.current) return;
service.current.openFavorite(id, target);
service.current.openFavorite(id, url, target);
},
[service],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const InstanceIdContext = createContext(getInstanceId());
* @param {object} props
* @param {import("preact").ComponentChild} props.children
* @param {T[]} props.items
* @param {(list: T[], id: string, index: number) => void} props.itemsDidReOrder
* @param {import('./FavoritesProvider.js').ReorderFn<{id: string; url: string}>} props.itemsDidReOrder
*/
export function PragmaticDND({ children, items, itemsDidReOrder }) {
/**
Expand All @@ -43,7 +43,7 @@ export function PragmaticDND({ children, items, itemsDidReOrder }) {
/**
* @template {{id: string; url: string}} T
* @param {T[]} favorites
* @param {(items: T[], id: string, target: number) => void} itemsDidReOrder
* @param {import('./FavoritesProvider.js').ReorderFn<{id: string; url: string}>} itemsDidReOrder
* @param {symbol} instanceId
*/
function useGridState(favorites, itemsDidReOrder, instanceId) {
Expand Down Expand Up @@ -89,7 +89,12 @@ function useGridState(favorites, itemsDidReOrder, instanceId) {
axis: 'horizontal',
});

itemsDidReOrder(favorites, id, targetIndex);
itemsDidReOrder({
list: favorites,
id,
fromIndex: favorites.length,
targetIndex,
});
},
}),
monitorForElements({
Expand Down Expand Up @@ -147,7 +152,12 @@ function useGridState(favorites, itemsDidReOrder, instanceId) {

flushSync(() => {
try {
itemsDidReOrder(reorderedList, startId, targetIndex);
itemsDidReOrder({
list: reorderedList,
id: startId,
fromIndex: startIndex,
targetIndex,
});
} catch (e) {
console.error('did catch', e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ export class FavoritesService {
* @param {FavoritesData} data
* @param {string} id - entity id to move
* @param {number} targetIndex - target index
* @param {number} fromIndex - from index
* @internal
*/
setFavoritesOrder(data, id, targetIndex) {
setFavoritesOrder(data, id, fromIndex, targetIndex) {
// update in memory instantly - this will broadcast changes to all listeners

this.dataService.update((_old) => {
Expand All @@ -96,6 +97,7 @@ export class FavoritesService {
this.ntp.messaging.notify('favorites_move', {
id,
targetIndex,
fromIndex,
});
}

Expand All @@ -110,12 +112,13 @@ export class FavoritesService {

/**
* @param {string} id - entity id
* @param {string} url - target url
* @param {FavoritesOpenAction['target']} target
* @internal
*/
openFavorite(id, target) {
openFavorite(id, url, target) {
// let the native side know too
this.ntp.messaging.notify('favorites_open', { id, target });
this.ntp.messaging.notify('favorites_open', { id, url, target });
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ export class FavoritesPage {
async opensInNewTab() {
await this.nthFavorite(0).click({ modifiers: ['Meta'] });
const calls = await this.ntp.mocks.waitForCallCount({ method: 'favorites_open', count: 1 });
expect(calls[0].payload.params).toStrictEqual({ id: 'id-many-1', target: 'new-tab' });
expect(calls[0].payload.params).toStrictEqual({ id: 'id-many-1', url: 'https://example.com/?id=id-many-1', target: 'new-tab' });
}

async opensInNewWindow() {
await this.nthFavorite(0).click({ modifiers: ['Shift'] });
const calls = await this.ntp.mocks.waitForCallCount({ method: 'favorites_open', count: 1 });
expect(calls[0].payload.params).toStrictEqual({ id: 'id-many-1', target: 'new-window' });
expect(calls[0].payload.params).toStrictEqual({ id: 'id-many-1', url: 'https://example.com/?id=id-many-1', target: 'new-window' });
}

async opensInSameTab() {
await this.nthFavorite(0).click();
const calls = await this.ntp.mocks.waitForCallCount({ method: 'favorites_open', count: 1 });
expect(calls[0].payload.params).toStrictEqual({ id: 'id-many-1', target: 'same-tab' });
expect(calls[0].payload.params).toStrictEqual({ id: 'id-many-1', url: 'https://example.com/?id=id-many-1', target: 'same-tab' });
}

async addsAnItem() {
Expand Down Expand Up @@ -157,13 +157,13 @@ export class FavoritesPage {
return { id };
}

async sent({ id, targetIndex }) {
async sent({ id, fromIndex, targetIndex }) {
const calls = await this.ntp.mocks.waitForCallCount({ method: 'favorites_move', count: 1 });
expect(calls[0].payload).toStrictEqual({
context: 'specialPages',
featureName: 'newTabPage',
method: 'favorites_move',
params: { id, targetIndex },
params: { id, fromIndex, targetIndex },
});
}

Expand Down Expand Up @@ -259,6 +259,7 @@ export class FavoritesPage {
params: {
id: '3',
targetIndex: index,
fromIndex: 15, // this is the length of the list, and it gets dropped at the end in the test.
},
});
}
Expand Down
Loading

0 comments on commit 7876fb9

Please sign in to comment.