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+
110export 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