Skip to content

Commit 62388bb

Browse files
Merge pull request #534 from Devin-AXIS/Carrie
Add monochrome plugin logos and normalize visual sizing
2 parents 2115714 + fe29b5a commit 62388bb

9 files changed

Lines changed: 165 additions & 3 deletions

File tree

‎apps/app/public/ext-douyin.png‎

161 KB
Loading

‎apps/app/public/ext-onto.png‎

166 KB
Loading
164 KB
Loading
215 KB
Loading
191 KB
Loading
214 KB
Loading

‎apps/app/src/app/index.css‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,50 @@ img[src$="/ext-video-console.png"] {
3131
scale: 1.2;
3232
}
3333

34+
/* Normalize the visible bounds and centers of the transparent plugin marks. */
35+
img[src$="/ext-xiaohongshu.png"] {
36+
scale: 1.2;
37+
translate: 0 -3.2%;
38+
}
39+
40+
img[src$="/ext-douyin.png"] {
41+
scale: 1.455;
42+
translate: 0 -4%;
43+
}
44+
45+
img[src$="/ext-wechat-channels.png"] {
46+
scale: 1.25;
47+
translate: 0 -2.2%;
48+
}
49+
50+
img[src$="/ext-operation-recorder.png"] {
51+
scale: 1.4;
52+
translate: -3.7% -5.5%;
53+
}
54+
55+
img[src$="/ext-onto.png"] {
56+
scale: 1.25;
57+
translate: 0 -4%;
58+
}
59+
60+
img[src$="/ext-short-video.png"] {
61+
scale: 1.35;
62+
translate: 0 -1.1%;
63+
}
64+
65+
/* Keep monochrome plugin marks readable on both dark and light icon tiles. */
66+
:is(.dark, [data-theme="dark"]) img:is(
67+
[src$="/ext-xiaohongshu.png"],
68+
[src$="/ext-douyin.png"],
69+
[src$="/ext-wechat-channels.png"],
70+
[src$="/ext-operation-recorder.png"],
71+
[src$="/ext-onto.png"],
72+
[src$="/ext-short-video.png"]
73+
) {
74+
filter: invert(1);
75+
mix-blend-mode: difference;
76+
}
77+
3478
@custom-variant dark (&:is(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *));
3579
@custom-variant electron {
3680
html.ipollowork-electron & {

‎apps/app/src/react-app/design-system/extension-icon-src.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
const featuredPluginIconSrc: Readonly<Record<string, string>> = {
22
context7: "/ext-context7.svg",
33
"design-agent": "/ext-design.png",
4+
"douyin-ops": "/ext-douyin.png",
45
figma: "/ext-figma.svg",
56
github: "/ext-github.svg",
67
"media-studio": "/ext-image-studio.png",
78
"image-studio": "/ext-image-studio.png",
9+
"ipollo-onto": "/ext-onto.png",
810
linear: "/ext-linear.svg",
911
notion: "/ext-notion.svg",
12+
"operation-recorder": "/ext-operation-recorder.png",
1013
sentry: "/ext-sentry.svg",
14+
"short-video-studio": "/ext-short-video.png",
1115
stripe: "/ext-stripe.svg",
1216
"video-agent": "/ext-video.png",
1317
"video-console": "/ext-video-console.png",
18+
"wechat-channels-ops": "/ext-wechat-channels.png",
19+
"xiaohongshu-ops": "/ext-xiaohongshu.png",
1420
};
1521

1622
export function resolveExtensionIconSrc(iconSrc: string): string {

‎evals/flows/plugin-library-refresh.flow.mjs‎

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
const pluginLogos = [
2+
{ name: "Xiaohongshu Operations", localizedName: "小红书运营台", file: "ext-xiaohongshu.png" },
3+
{ name: "Douyin Operations", localizedName: "抖音运营台", file: "ext-douyin.png" },
4+
{ name: "WeChat Channels Operations", localizedName: "视频号运营台", file: "ext-wechat-channels.png" },
5+
{ name: "Operation Recorder", localizedName: "操作录制", file: "ext-operation-recorder.png" },
6+
{ name: "iPollo Onto", localizedName: "iPollo Onto", file: "ext-onto.png" },
7+
{ name: "Short Video Studio", localizedName: "短片创作工作台", file: "ext-short-video.png" },
8+
];
9+
110
export default {
211
id: "plugin-library-refresh",
312
title: "Refresh the installed plugin library",
413
kind: "user-facing",
514
steps: [{
615
name: "Refresh plugins without losing the installed library",
716
run: async (ctx) => {
8-
await ctx.navigateHash("/settings/extensions");
17+
await ctx.waitFor("Boolean(window.__ipolloworkControl)");
18+
await ctx.control("settings.panel.open", { panel: "extensions" });
919
await ctx.waitFor(`document.querySelector('[data-testid="plugin-installed-tile"]') !== null`, {
1020
timeoutMs: 30_000,
1121
label: "installed plugins",
@@ -36,6 +46,11 @@ export default {
3646
timeoutMs: 30_000,
3747
label: "both plugin list responses",
3848
});
49+
await ctx.waitFor(`Boolean([...document.querySelectorAll('[data-testid="plugin-package-list-item"] img')]
50+
.find(img => img.src.endsWith('/ext-xiaohongshu.png') && img.complete && img.naturalWidth > 0))`, {
51+
timeoutMs: 30_000,
52+
label: "bundled catalog rendered after refresh",
53+
});
3954
},
4055
assert: async () => {
4156
const result = await ctx.eval(`({
@@ -47,10 +62,107 @@ export default {
4762
ctx.assert(result.responses.some(item => item.catalog && item.status === 200 && item.count > 0), "Bundled catalog did not load.");
4863
ctx.assert(result.installed > 0 && !result.error, "Plugin cards or error state are incorrect.");
4964
},
50-
screenshot: "plugin-library-refreshed",
65+
screenshot: {
66+
name: "plugin-library-refreshed",
67+
requireText: ["iPolloWork"],
68+
rejectText: ["Failed to load plugin packages", "无法加载插件包", "Something went wrong"],
69+
},
5170
}).finally(async () => {
5271
await ctx.eval(`window.__restorePluginRefreshFetch?.(); delete window.__restorePluginRefreshFetch; delete window.__pluginRefreshProof;`);
5372
});
5473
},
55-
}],
74+
}, ...["light", "dark"].flatMap((theme) => pluginLogos.map((plugin) => ({
75+
name: `Show ${plugin.name}'s approved logo in ${theme} mode`,
76+
run: async (ctx) => {
77+
let visibleName;
78+
await ctx.prove(`${plugin.name} displays its own local PNG in the ${theme} plugin catalog`, {
79+
voiceover: `搜索${plugin.localizedName},${theme === "dark" ? "深色" : "浅色"}列表显示对应的黑白图标,便于辨认插件。`,
80+
action: async () => {
81+
if (await ctx.eval("document.documentElement.dataset.theme") !== theme) {
82+
await ctx.control("settings.panel.open", { panel: "appearance" });
83+
const labels = theme === "dark" ? ["Dark", "深色"] : ["Light", "浅色"];
84+
await ctx.waitFor(`Boolean([...document.querySelectorAll('button')]
85+
.find(button => ${JSON.stringify(labels)}.includes(button.getAttribute('aria-label'))))`);
86+
await ctx.eval(`([...document.querySelectorAll('button')]
87+
.find(button => ${JSON.stringify(labels)}.includes(button.getAttribute('aria-label')))).click()`);
88+
await ctx.waitFor(`document.documentElement.dataset.theme === ${JSON.stringify(theme)}`);
89+
await ctx.control("settings.panel.open", { panel: "extensions" });
90+
await ctx.waitFor(`Boolean(document.querySelector('input[aria-label="Search plugins"], input[aria-label="搜索插件"]'))`);
91+
}
92+
await ctx.eval(`(() => {
93+
[...document.querySelectorAll('[role="tab"]')]
94+
.find(tab => ['个人', 'Personal'].includes(tab.textContent.trim()))?.click();
95+
})()`);
96+
await ctx.fill('input[aria-label="Search plugins"], input[aria-label="搜索插件"]', "");
97+
await ctx.waitFor(`document.body.innerText.includes(${JSON.stringify(plugin.name)})
98+
|| document.body.innerText.includes(${JSON.stringify(plugin.localizedName)})`, { timeoutMs: 30_000 });
99+
visibleName = await ctx.eval(`document.body.innerText.includes(${JSON.stringify(plugin.name)})
100+
? ${JSON.stringify(plugin.name)} : ${JSON.stringify(plugin.localizedName)}`);
101+
await ctx.fill('input[aria-label="Search plugins"], input[aria-label="搜索插件"]', visibleName);
102+
await ctx.waitFor(`Boolean([...document.querySelectorAll('[data-testid="plugin-package-list-item"] img')]
103+
.find(img => img.src.endsWith(${JSON.stringify(plugin.file)}) && img.complete && img.naturalWidth > 0))`, {
104+
timeoutMs: 15_000,
105+
label: `${plugin.name} loaded logo`,
106+
});
107+
},
108+
assert: async () => {
109+
const logo = await ctx.eval(`(() => {
110+
const img = [...document.querySelectorAll('[data-testid="plugin-package-list-item"] img')]
111+
.find(img => img.src.endsWith(${JSON.stringify(plugin.file)}));
112+
const row = img?.closest('[data-testid="plugin-package-list-item"]');
113+
return { loaded: Boolean(img?.complete && img.naturalWidth > 0),
114+
ownRow: Boolean(row?.innerText.includes(${JSON.stringify(plugin.name)})
115+
|| row?.innerText.includes(${JSON.stringify(plugin.localizedName)})),
116+
visible: Boolean(img?.getClientRects().length) };
117+
})()`);
118+
ctx.assert(logo.loaded && logo.ownRow && logo.visible, `${plugin.name} logo is missing or on the wrong row.`);
119+
const bounds = await ctx.eval(`(async () => {
120+
const img = [...document.querySelectorAll('[data-testid="plugin-package-list-item"] img')]
121+
.find(img => img.src.endsWith(${JSON.stringify(plugin.file)}));
122+
await img.decode();
123+
const canvas = document.createElement('canvas');
124+
canvas.width = img.naturalWidth;
125+
canvas.height = img.naturalHeight;
126+
const context = canvas.getContext('2d');
127+
context.drawImage(img, 0, 0);
128+
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
129+
let left = canvas.width, top = canvas.height, right = 0, bottom = 0;
130+
for (let y = 0; y < canvas.height; y++) for (let x = 0; x < canvas.width; x++) {
131+
if (pixels[(y * canvas.width + x) * 4 + 3] < 128) continue;
132+
left = Math.min(left, x); top = Math.min(top, y);
133+
right = Math.max(right, x + 1); bottom = Math.max(bottom, y + 1);
134+
}
135+
const rect = img.getBoundingClientRect();
136+
const tile = img.parentElement.getBoundingClientRect();
137+
const visibleWidth = (right - left) / canvas.width * rect.width;
138+
const visibleHeight = (bottom - top) / canvas.height * rect.height;
139+
const centerX = rect.left + (left + right) / 2 / canvas.width * rect.width;
140+
const centerY = rect.top + (top + bottom) / 2 / canvas.height * rect.height;
141+
return {
142+
visibleWidth, visibleHeight,
143+
sizeRatio: Math.max(visibleWidth, visibleHeight) / parseFloat(getComputedStyle(img).width),
144+
centerOffset: Math.max(Math.abs(centerX - tile.left - tile.width / 2), Math.abs(centerY - tile.top - tile.height / 2)),
145+
contained: centerX - visibleWidth / 2 >= tile.left && centerX + visibleWidth / 2 <= tile.right
146+
&& centerY - visibleHeight / 2 >= tile.top && centerY + visibleHeight / 2 <= tile.bottom,
147+
};
148+
})()`, { awaitPromise: true });
149+
ctx.assert(bounds.sizeRatio >= 0.95 && bounds.sizeRatio <= 1.05, `${plugin.name} visible mark does not match adjacent full-size logos: ${JSON.stringify(bounds)}`);
150+
ctx.assert(bounds.centerOffset < 0.75 && bounds.contained, `${plugin.name} mark is off-center or clipped: ${JSON.stringify(bounds)}`);
151+
if (theme === "dark") {
152+
const styles = await ctx.eval(`(() => {
153+
const img = [...document.querySelectorAll('[data-testid="plugin-package-list-item"] img')]
154+
.find(img => img.src.endsWith(${JSON.stringify(plugin.file)}));
155+
return { filter: getComputedStyle(img).filter, blend: getComputedStyle(img).mixBlendMode };
156+
})()`);
157+
ctx.assert(styles.filter === "invert(1)" && styles.blend === "difference", `${plugin.name} lacks dark background contrast.`);
158+
}
159+
},
160+
screenshot: {
161+
name: `${plugin.file.replace(".png", "")}-${theme}`,
162+
get requireText() { return [visibleName]; },
163+
rejectText: ["Something went wrong"],
164+
},
165+
});
166+
},
167+
})))],
56168
};

0 commit comments

Comments
 (0)