Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

1648 lines
76 KiB
JavaScript

import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { getRegistry } from '@jackwener/opencli/registry';
import * as privatePublish from './_shared/private-publish.js';
import { buildClickActionJs, buildEnsureComposerOpenJs, buildInspectUploadStageJs, buildPublishStatusProbeJs } from './post.js';
import './post.js';
const tempDirs = [];
function createTempImage(name = 'demo.jpg', bytes = Buffer.from([0xff, 0xd8, 0xff, 0xd9])) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-post-'));
tempDirs.push(dir);
const filePath = path.join(dir, name);
fs.writeFileSync(filePath, bytes);
return filePath;
}
function createTempVideo(name = 'demo.mp4', bytes = Buffer.from('video')) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-post-video-'));
tempDirs.push(dir);
const filePath = path.join(dir, name);
fs.writeFileSync(filePath, bytes);
return filePath;
}
function withInitialDialogDismiss(results) {
return [{ ok: false }, ...results];
}
function createPageMock(evaluateResults, overrides = {}) {
const evaluate = vi.fn();
for (const result of evaluateResults) {
evaluate.mockResolvedValueOnce(result);
}
return {
goto: vi.fn().mockResolvedValue(undefined),
evaluate,
getCookies: vi.fn().mockResolvedValue([]),
snapshot: vi.fn().mockResolvedValue(undefined),
click: vi.fn().mockResolvedValue(undefined),
typeText: vi.fn().mockResolvedValue(undefined),
pressKey: vi.fn().mockResolvedValue(undefined),
scrollTo: vi.fn().mockResolvedValue(undefined),
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
wait: vi.fn().mockResolvedValue(undefined),
tabs: vi.fn().mockResolvedValue([]),
closeTab: vi.fn().mockResolvedValue(undefined),
newTab: vi.fn().mockResolvedValue(undefined),
selectTab: vi.fn().mockResolvedValue(undefined),
networkRequests: vi.fn().mockResolvedValue([]),
consoleMessages: vi.fn().mockResolvedValue([]),
scroll: vi.fn().mockResolvedValue(undefined),
autoScroll: vi.fn().mockResolvedValue(undefined),
installInterceptor: vi.fn().mockResolvedValue(undefined),
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: undefined,
getCurrentUrl: vi.fn().mockResolvedValue(null),
...overrides,
};
}
afterAll(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
delete process.env.OPENCLI_INSTAGRAM_CAPTURE;
});
describe('instagram auth detection', () => {
it('does not treat generic homepage text containing "log in" as an auth failure', () => {
const globalState = globalThis;
const originalDocument = globalState.document;
const originalWindow = globalState.window;
globalState.document = {
body: { innerText: 'Suggested for you Log in to see more content' },
querySelector: () => null,
querySelectorAll: () => [],
};
globalState.window = { location: { pathname: '/' } };
try {
expect(eval(buildEnsureComposerOpenJs())).toEqual({ ok: true });
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
}
});
});
describe('instagram publish status detection', () => {
it('does not treat unrelated page text as share failure while the sharing dialog is still visible', () => {
const globalState = globalThis;
class MockHTMLElement {
}
const visibleDialog = new MockHTMLElement();
visibleDialog.textContent = 'Sharing';
visibleDialog.querySelector = () => null;
visibleDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
const originalDocument = globalState.document;
const originalWindow = globalState.window;
const originalHTMLElement = globalState.HTMLElement;
globalState.HTMLElement = MockHTMLElement;
globalState.document = {
querySelectorAll: (selector) => selector === '[role="dialog"]' ? [visibleDialog] : [],
};
globalState.window = {
location: { href: 'https://www.instagram.com/' },
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
try {
expect(eval(buildPublishStatusProbeJs())).toEqual({
ok: false,
failed: false,
settled: false,
url: '',
});
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
globalState.HTMLElement = originalHTMLElement;
}
});
it('does not treat a stale visible error dialog as share failure while sharing is still in progress', () => {
const globalState = globalThis;
class MockHTMLElement {
}
const sharingDialog = new MockHTMLElement();
sharingDialog.textContent = 'Sharing';
sharingDialog.querySelector = () => null;
sharingDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
const staleErrorDialog = new MockHTMLElement();
staleErrorDialog.textContent = 'Something went wrong. Please try again. Try again';
staleErrorDialog.querySelector = () => null;
staleErrorDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
const originalDocument = globalState.document;
const originalWindow = globalState.window;
const originalHTMLElement = globalState.HTMLElement;
globalState.HTMLElement = MockHTMLElement;
globalState.document = {
querySelectorAll: (selector) => selector === '[role="dialog"]' ? [sharingDialog, staleErrorDialog] : [],
};
globalState.window = {
location: { href: 'https://www.instagram.com/' },
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
try {
expect(eval(buildPublishStatusProbeJs())).toEqual({
ok: false,
failed: false,
settled: false,
url: '',
});
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
globalState.HTMLElement = originalHTMLElement;
}
});
it('prefers explicit post-shared success over stale visible error text', () => {
const globalState = globalThis;
class MockHTMLElement {
}
const sharedDialog = new MockHTMLElement();
sharedDialog.textContent = 'Post shared Your post has been shared.';
sharedDialog.querySelector = () => null;
sharedDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
const staleErrorDialog = new MockHTMLElement();
staleErrorDialog.textContent = 'Something went wrong. Please try again. Try again';
staleErrorDialog.querySelector = () => null;
staleErrorDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
const originalDocument = globalState.document;
const originalWindow = globalState.window;
const originalHTMLElement = globalState.HTMLElement;
globalState.HTMLElement = MockHTMLElement;
globalState.document = {
querySelectorAll: (selector) => selector === '[role="dialog"]' ? [sharedDialog, staleErrorDialog] : [],
};
globalState.window = {
location: { href: 'https://www.instagram.com/' },
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
try {
expect(eval(buildPublishStatusProbeJs())).toEqual({
ok: true,
failed: false,
settled: false,
url: '',
});
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
globalState.HTMLElement = originalHTMLElement;
}
});
});
describe('instagram click action detection', () => {
it('matches aria-label-only Next buttons in the media dialog', () => {
const globalState = globalThis;
class MockHTMLElement {
textContent = '';
ariaLabel = '';
clicked = false;
querySelectorAll = (_selector) => [];
querySelector = (_selector) => null;
getAttribute(name) {
if (name === 'aria-label')
return this.ariaLabel || null;
return null;
}
getBoundingClientRect() {
return { width: 100, height: 40 };
}
click() {
this.clicked = true;
}
}
const nextButton = new MockHTMLElement();
nextButton.ariaLabel = 'Next';
const dialog = new MockHTMLElement();
dialog.textContent = 'Crop Back Select crop Open media gallery';
dialog.querySelector = (selector) => selector === 'input[type="file"]' ? {} : null;
dialog.querySelectorAll = (selector) => selector === 'button, div[role="button"]' ? [nextButton] : [];
const body = new MockHTMLElement();
const originalDocument = globalState.document;
const originalWindow = globalState.window;
const originalHTMLElement = globalState.HTMLElement;
globalState.HTMLElement = MockHTMLElement;
globalState.document = {
body,
querySelectorAll: (selector) => selector === '[role="dialog"]' ? [dialog] : [],
};
globalState.window = {
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
try {
expect(eval(buildClickActionJs(['Next', '下一步'], 'media'))).toEqual({
ok: true,
label: 'Next',
});
expect(nextButton.clicked).toBe(true);
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
globalState.HTMLElement = originalHTMLElement;
}
});
it('does not click a body-level Next button when media scope has no matching dialog controls', () => {
const globalState = globalThis;
class MockHTMLElement {
textContent = '';
ariaLabel = '';
clicked = false;
children = [];
querySelectorAll = (_selector) => this.children;
querySelector = (_selector) => null;
getAttribute(name) {
if (name === 'aria-label')
return this.ariaLabel || null;
return null;
}
getBoundingClientRect() {
return { width: 100, height: 40 };
}
click() {
this.clicked = true;
}
}
const bodyNext = new MockHTMLElement();
bodyNext.ariaLabel = 'Next';
const errorDialog = new MockHTMLElement();
errorDialog.textContent = 'Something went wrong Try again';
errorDialog.children = [];
const body = new MockHTMLElement();
body.children = [bodyNext];
const originalDocument = globalState.document;
const originalWindow = globalState.window;
const originalHTMLElement = globalState.HTMLElement;
globalState.HTMLElement = MockHTMLElement;
globalState.document = {
body,
querySelectorAll: (selector) => selector === '[role="dialog"]' ? [errorDialog] : [],
};
globalState.window = {
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
try {
expect(eval(buildClickActionJs(['Next', '下一步'], 'media'))).toEqual({ ok: false });
expect(bodyNext.clicked).toBe(false);
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
globalState.HTMLElement = originalHTMLElement;
}
});
});
describe('instagram upload stage detection', () => {
it('does not treat a body-level Next button as upload preview when the visible dialog is an error', () => {
const globalState = globalThis;
class MockHTMLElement {
textContent = '';
ariaLabel = '';
children = [];
querySelectorAll = (_selector) => this.children;
querySelector = (_selector) => null;
getAttribute(name) {
if (name === 'aria-label')
return this.ariaLabel || null;
return null;
}
getBoundingClientRect() {
return { width: 100, height: 40 };
}
}
const bodyNext = new MockHTMLElement();
bodyNext.ariaLabel = 'Next';
const errorDialog = new MockHTMLElement();
errorDialog.textContent = 'Something went wrong. Please try again. Try again';
const body = new MockHTMLElement();
body.children = [bodyNext];
const originalDocument = globalState.document;
const originalWindow = globalState.window;
const originalHTMLElement = globalState.HTMLElement;
globalState.HTMLElement = MockHTMLElement;
globalState.document = {
body,
querySelectorAll: (selector) => {
if (selector === '[role="dialog"]')
return [errorDialog];
return [];
},
};
globalState.window = {
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
try {
expect(eval(buildInspectUploadStageJs())).toEqual({
state: 'failed',
detail: 'Something went wrong. Please try again. Try again',
});
}
finally {
globalState.document = originalDocument;
globalState.window = originalWindow;
globalState.HTMLElement = originalHTMLElement;
}
});
});
describe('instagram post registration', () => {
beforeEach(() => {
vi.spyOn(privatePublish, 'resolveInstagramPrivatePublishConfig').mockResolvedValue({
apiContext: {
asbdId: '',
csrfToken: 'csrf-token',
igAppId: '936619743392459',
igWwwClaim: '',
instagramAjax: '1036523242',
webSessionId: '',
},
jazoest: '22047',
});
vi.spyOn(privatePublish, 'publishImagesViaPrivateApi').mockRejectedValue(new CommandExecutionError('Instagram private publish configure failed: 400 {"message":"fallback to ui"}'));
});
afterEach(() => {
vi.restoreAllMocks();
});
it('registers the post command with a required-value media arg', () => {
const cmd = getRegistry().get('instagram/post');
expect(cmd).toBeDefined();
expect(cmd?.browser).toBe(true);
expect(cmd?.args.some((arg) => arg.name === 'timeout' && arg.default === 300)).toBe(true);
expect(cmd?.args.some((arg) => arg.name === 'media' && !arg.required && arg.valueRequired)).toBe(true);
expect(cmd?.args.some((arg) => arg.name === 'content' && !arg.required && arg.positional)).toBe(true);
});
it('prefers the private route by default and returns without touching UI upload steps when private publish succeeds', async () => {
const imagePath = createTempImage('private-default.jpg');
const privateSpy = vi.spyOn(privatePublish, 'publishImagesViaPrivateApi').mockResolvedValueOnce({
code: 'PRIVATEDEFAULT123',
uploadIds: ['111'],
});
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture'))
return { data: [], errors: [] };
return { ok: true };
});
const page = createPageMock([], {
evaluate,
getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, { media: imagePath, content: 'private default' });
expect(privateSpy).toHaveBeenCalledTimes(1);
expect(page.setFileInput).not.toHaveBeenCalled();
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/PRIVATEDEFAULT123/',
},
]);
privateSpy.mockRestore();
});
it('falls back to the UI route when the default private route fails safely before publishing', async () => {
const imagePath = createTempImage('private-fallback-ui.jpg');
const privateSpy = vi.spyOn(privatePublish, 'publishImagesViaPrivateApi').mockRejectedValueOnce(new CommandExecutionError('Instagram private publish configure_sidecar failed: 400 {"message":"Uploaded image is invalid"}'));
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/PRIVATEFALLBACK123/' };
return { ok: true };
});
const page = createPageMock([], {
evaluate,
getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, { media: imagePath, content: 'private fallback' });
expect(privateSpy).toHaveBeenCalledTimes(1);
expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-opencli-ig-upload-index="0"]');
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/PRIVATEFALLBACK123/',
},
]);
privateSpy.mockRestore();
});
it('falls back to the UI route for mixed-media posts when the private route fails safely before publishing', async () => {
const imagePath = createTempImage('mixed-fallback-image.jpg');
const videoPath = createTempVideo('mixed-fallback-video.mp4');
const privateSpy = vi.spyOn(privatePublish, 'publishMediaViaPrivateApi').mockRejectedValueOnce(new CommandExecutionError('Instagram private publish configure_sidecar failed: 400 {"message":"fallback to ui"}'));
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/MIXEDFALLBACK123/' };
return { ok: true };
});
const page = createPageMock([], {
evaluate,
getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: `${imagePath},${videoPath}`,
content: 'mixed ui fallback',
});
expect(privateSpy).toHaveBeenCalledTimes(1);
expect(page.setFileInput).toHaveBeenCalledWith([imagePath, videoPath], '[data-opencli-ig-upload-index="0"]');
expect(result).toEqual([
{
status: '✅ Posted',
detail: '2-item mixed-media carousel post shared successfully',
url: 'https://www.instagram.com/p/MIXEDFALLBACK123/',
},
]);
privateSpy.mockRestore();
});
it('prefers the private route by default for mixed-media posts and preserves input order', async () => {
const imagePath = createTempImage('mixed-default.jpg');
const videoPath = createTempVideo('mixed-default.mp4');
const privateSpy = vi.spyOn(privatePublish, 'publishMediaViaPrivateApi').mockResolvedValueOnce({
code: 'MIXEDPRIVATE123',
uploadIds: ['111', '222'],
});
const page = createPageMock([], {
evaluate: vi.fn(async () => ({ ok: true })),
getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: `${imagePath},${videoPath}`,
content: 'mixed private default',
});
expect(privateSpy).toHaveBeenCalledWith(expect.objectContaining({
mediaItems: [
{ type: 'image', filePath: imagePath },
{ type: 'video', filePath: videoPath },
],
caption: 'mixed private default',
}));
expect(page.setFileInput).not.toHaveBeenCalled();
expect(result).toEqual([
{
status: '✅ Posted',
detail: '2-item mixed-media carousel post shared successfully',
url: 'https://www.instagram.com/p/MIXEDPRIVATE123/',
},
]);
privateSpy.mockRestore();
});
it('rejects missing --media before browser work', async () => {
const page = createPageMock([]);
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
content: 'missing media',
})).rejects.toThrow('Argument "media" is required.');
});
it('rejects empty or invalid --media inputs', async () => {
const imagePath = createTempImage('invalid-media-image.jpg');
const page = createPageMock([]);
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: '',
})).rejects.toThrow('Argument "media" is required.');
await expect(cmd.func(page, {
media: `${imagePath},/tmp/does-not-exist.mp4`,
})).rejects.toThrow('Media file not found');
});
it('uploads a single image, fills caption, and shares the post', async () => {
const imagePath = createTempImage();
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/ABC123xyz/' },
]));
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'hello from opencli',
});
expect(page.goto).toHaveBeenCalledWith('https://www.instagram.com/');
expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-opencli-ig-upload-index="0"]');
expect(page.evaluate.mock.calls.some((args) => String(args[0]).includes("dispatchEvent(new Event('change'"))).toBe(true);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/ABC123xyz/',
},
]);
});
it('uploads multiple images as a carousel and shares the post', async () => {
const firstImagePath = createTempImage('carousel-1.jpg');
const secondImagePath = createTempImage('carousel-2.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/CAROUSEL123/' },
]));
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: `${firstImagePath},${secondImagePath}`,
content: 'hello carousel',
});
expect(page.setFileInput).toHaveBeenCalledWith([firstImagePath, secondImagePath], '[data-opencli-ig-upload-index="0"]');
expect(result).toEqual([
{
status: '✅ Posted',
detail: '2-image carousel post shared successfully',
url: 'https://www.instagram.com/p/CAROUSEL123/',
},
]);
});
it('installs and dumps protocol capture when OPENCLI_INSTAGRAM_CAPTURE is enabled', async () => {
process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
const imagePath = createTempImage('capture-enabled.jpg');
const evaluate = vi.fn(async (js) => {
if (js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))
return { ok: true };
if (js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture')) {
return { data: [], errors: [] };
}
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/CAPTURE123/' };
return { ok: true };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'capture enabled',
});
const evaluateCalls = evaluate.mock.calls.map((args) => String(args[0]));
expect(evaluateCalls.some((js) => js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(true);
expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture'))).toBe(true);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/CAPTURE123/',
},
]);
delete process.env.OPENCLI_INSTAGRAM_CAPTURE;
});
it('retries media Next when preview is visible before the button becomes clickable', async () => {
const firstImagePath = createTempImage('carousel-delay-1.jpg');
const secondImagePath = createTempImage('carousel-delay-2.jpg');
let nextAttempts = 0;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasVisibleButtonInDialogs'))
return { state: 'preview', detail: 'Crop Back Next Select crop' };
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: nextAttempts >= 2 };
}
if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
nextAttempts += 1;
if (nextAttempts === 1)
return { ok: false };
return { ok: true, label: 'Next' };
}
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/CAROUSELRETRY123/' };
return { ok: true };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: `${firstImagePath},${secondImagePath}`,
content: 'hello delayed carousel',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: '2-image carousel post shared successfully',
url: 'https://www.instagram.com/p/CAROUSELRETRY123/',
},
]);
});
it('retries the whole carousel flow when preview briefly appears and then degrades into an upload error before Next is usable', async () => {
const firstImagePath = createTempImage('carousel-race-1.jpg');
const secondImagePath = createTempImage('carousel-race-2.jpg');
let composerRuns = 0;
let uploadStageChecks = 0;
let secondAttemptAdvanced = false;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname')) {
composerRuns += 1;
return { ok: true };
}
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasVisibleButtonInDialogs')) {
uploadStageChecks += 1;
if (composerRuns === 1 && uploadStageChecks === 1) {
return { state: 'preview', detail: 'Crop Back Next Select crop' };
}
if (composerRuns === 1) {
return { state: 'failed', detail: 'Something went wrong. Please try again.' };
}
return { state: 'preview', detail: 'Crop Back Next Select crop' };
}
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: composerRuns >= 2 && secondAttemptAdvanced };
}
if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
if (composerRuns === 1)
return { ok: false };
secondAttemptAdvanced = true;
return { ok: true, label: 'Next' };
}
if (js.includes('button[aria-label="Close"]'))
return { ok: true };
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/CAROUSELFRESH123/' };
return { ok: true };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: `${firstImagePath},${secondImagePath}`,
content: 'hello recovered carousel',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: '2-image carousel post shared successfully',
url: 'https://www.instagram.com/p/CAROUSELFRESH123/',
},
]);
});
it('uploads a single image and shares it without a caption when content is omitted', async () => {
const imagePath = createTempImage('no-caption.jpg');
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('const editable = document.querySelector(\'textarea, [contenteditable="true"]\');'))
return { ok: true };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/NOCAPTION123/' };
return { ok: false };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
});
const evaluateCalls = page.evaluate.mock.calls.map((args) => String(args[0]));
expect(evaluateCalls.some((js) => js.includes('Write a caption'))).toBe(false);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/NOCAPTION123/',
},
]);
});
it('falls back to browser-side file injection when the extension does not support set-file-input', async () => {
const imagePath = createTempImage('legacy-extension.jpg');
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes('__opencliInstagramUpload_') && js.includes('] = [];'))
return { ok: true };
if (js.includes('parts.push(chunk)'))
return { ok: true, count: 1 };
if (js.includes('File input not found for fallback injection'))
return { ok: true, count: 1 };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes('labels.includes(text)'))
return { ok: false };
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('couldn') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/LEGACY123/' };
return { ok: true };
});
const page = createPageMock([], {
evaluate,
setFileInput: vi.fn().mockRejectedValue(new Error('Unknown action: set-file-input')),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'legacy bridge fallback',
});
expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-opencli-ig-upload-index="0"]');
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/LEGACY123/',
},
]);
});
it('chunks large legacy fallback uploads instead of embedding the whole image in one evaluate payload', async () => {
const imagePath = createTempImage('legacy-large.jpg', Buffer.alloc(900 * 1024, 1));
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes('window[') && js.includes('] = [];'))
return { ok: true };
if (js.includes('parts.push(chunk)'))
return { ok: true, count: 1 };
if (js.includes('File input not found for fallback injection'))
return { ok: true, count: 1 };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes('labels.includes(text)'))
return { ok: false };
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('couldn') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/LARGELEGACY123/' };
return { ok: true };
});
const page = createPageMock([], {
evaluate,
setFileInput: vi.fn().mockRejectedValue(new Error('Unknown action: set-file-input')),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'legacy large bridge fallback',
});
const chunkCalls = evaluate.mock.calls.filter((args) => String(args[0]).includes('parts.push(chunk)'));
expect(chunkCalls.length).toBeGreaterThan(1);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/LARGELEGACY123/',
},
]);
});
it('fails clearly when Browser Bridge file upload support is unavailable', async () => {
const imagePath = createTempImage('missing-bridge.jpg');
const page = createPageMock([], { setFileInput: undefined });
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: imagePath,
content: 'hello from opencli',
})).rejects.toThrow(CommandExecutionError);
});
it('maps login-gated composer access to AuthRequiredError', async () => {
const imagePath = createTempImage('auth.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: false, reason: 'auth' },
]));
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: imagePath,
content: 'login required',
})).rejects.toThrow(AuthRequiredError);
});
it('captures a debug screenshot when the upload preview never appears', async () => {
const imagePath = createTempImage('no-preview.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
]));
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: imagePath,
content: 'preview missing',
})).rejects.toThrow('Instagram image preview did not appear after upload');
expect(page.screenshot).toHaveBeenCalledWith({ path: '/tmp/instagram_post_preview_debug.png' });
});
it('fails clearly when Instagram shows an upload-stage error dialog', async () => {
const imagePath = createTempImage('upload-error.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' },
]));
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: imagePath,
content: 'upload should fail clearly',
})).rejects.toThrow('Instagram image upload failed');
});
it('treats crop/next preview UI as success even if stale error text is still visible', async () => {
const imagePath = createTempImage('upload-preview-wins.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{
ok: false,
state: 'preview',
detail: 'Something went wrong. Please try again. Crop Back Next Select crop Select zoom Open media gallery',
},
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/PREVIEWWINS123/' },
]));
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'preview state wins over stale error text',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/PREVIEWWINS123/',
},
]);
});
it('retries the same upload selector once after an upload-stage error and can still succeed', async () => {
const imagePath = createTempImage('upload-retry.jpg');
const setFileInput = vi.fn().mockResolvedValue(undefined);
let uploadProbeCount = 0;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const failed =') && js.includes('const hasCaption =')) {
uploadProbeCount += 1;
return uploadProbeCount === 1
? { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' }
: { ok: true, state: 'preview' };
}
if (js.includes('button[aria-label="Close"]'))
return { ok: true };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/UPLOADRETRY123/' };
return { ok: true };
});
const page = createPageMock([], { setFileInput, evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'upload retry succeeds',
});
expect(setFileInput).toHaveBeenCalledTimes(1);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/UPLOADRETRY123/',
},
]);
});
it('clicks upload Try again in-place before resetting the whole flow when Instagram shows an upload error dialog', async () => {
const imagePath = createTempImage('upload-inline-retry.jpg');
let uploadProbeCount = 0;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasVisibleButtonInDialogs')) {
uploadProbeCount += 1;
return uploadProbeCount === 1
? { state: 'failed', detail: 'Something went wrong. Please try again.' }
: { state: 'preview', detail: 'Crop Back Next Select crop' };
}
if (js.includes('something went wrong') && js.includes('label === \'try again\''))
return { ok: true };
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: true };
}
if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
if (js.includes('"Share"'))
return { ok: true, label: 'Share' };
return { ok: true, label: 'Next' };
}
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/UPLOADINLINERETRY123/' };
return { ok: true };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'upload inline retry succeeds',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/UPLOADINLINERETRY123/',
},
]);
});
it('retries max-size carousel upload failures beyond the expanded large-carousel budget before succeeding', async () => {
const paths = [
createTempImage('carousel-10-1.jpg'),
createTempImage('carousel-10-2.jpg'),
createTempImage('carousel-10-3.jpg'),
createTempImage('carousel-10-4.jpg'),
createTempImage('carousel-10-5.jpg'),
createTempImage('carousel-10-6.jpg'),
createTempImage('carousel-10-7.jpg'),
createTempImage('carousel-10-8.jpg'),
createTempImage('carousel-10-9.jpg'),
createTempImage('carousel-10-10.jpg'),
];
const setFileInput = vi.fn().mockResolvedValue(undefined);
let uploadProbeCount = 0;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasVisibleButtonInDialogs')) {
uploadProbeCount += 1;
if (uploadProbeCount <= 16) {
return { state: 'failed', detail: 'Something went wrong. Please try again.' };
}
return { state: 'preview', detail: 'Crop Back Next Select crop' };
}
if (js.includes('button[aria-label="Close"]'))
return { ok: true };
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: true };
}
if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
if (js.includes('"Share"'))
return { ok: true, label: 'Share' };
return { ok: true, label: 'Next' };
}
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/CAROUSEL10RETRY123/' };
return { ok: true };
});
const page = createPageMock([], { setFileInput, evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: paths.join(','),
});
expect(setFileInput).toHaveBeenCalledTimes(5);
expect(result).toEqual([
{
status: '✅ Posted',
detail: '10-image carousel post shared successfully',
url: 'https://www.instagram.com/p/CAROUSEL10RETRY123/',
},
]);
});
it('forces a fresh home reload before retrying after an upload-stage error', async () => {
const imagePath = createTempImage('upload-fresh-reload.jpg');
const gotoUrls = [];
const goto = vi.fn(async (url) => {
gotoUrls.push(String(url));
});
let uploadProbeCount = 0;
let advancedToCaption = false;
const evaluate = vi.fn(async (js) => {
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: advancedToCaption };
}
if (js.includes('const hasPreviewUi =')) {
uploadProbeCount += 1;
if (uploadProbeCount === 1) {
return { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' };
}
return gotoUrls.some((url) => url.includes('__opencli_reset='))
? { ok: true, state: 'preview' }
: { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' };
}
if (js.includes('button[aria-label="Close"]'))
return { ok: false };
if (js.includes("scope === 'media'")) {
advancedToCaption = true;
return { ok: true, label: 'Next' };
}
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/FRESHRELOAD123/' };
return { ok: false };
});
const page = createPageMock([], {
goto,
evaluate,
setFileInput: vi.fn().mockResolvedValue(undefined),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'fresh reload after upload failure',
});
expect(gotoUrls.some((url) => url.includes('__opencli_reset='))).toBe(true);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/FRESHRELOAD123/',
},
]);
});
it('retries the share action in-place when Instagram shows a visible try-again share failure dialog', async () => {
const imagePath = createTempImage('share-retry.jpg');
let shareStatusChecks = 0;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasVisibleButtonInDialogs'))
return { state: 'preview', detail: 'Crop Back Next Select crop' };
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: true };
}
if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
if (js.includes('"Share"'))
return { ok: true, label: 'Share' };
return { ok: true, label: 'Next' };
}
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('post shared') && js.includes('your post has been shared')) {
shareStatusChecks += 1;
return shareStatusChecks === 1
? { ok: false, failed: true, settled: false, url: '' }
: { ok: true, failed: false, settled: false, url: 'https://www.instagram.com/p/SHARERETRY123/' };
}
if (js.includes('post couldn') && js.includes('try again'))
return { ok: true };
return { ok: true };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'share retry succeeds',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/SHARERETRY123/',
},
]);
});
it('re-resolves the upload input when the tagged selector goes stale before setFileInput runs', async () => {
const imagePath = createTempImage('stale-selector.jpg');
const setFileInput = vi.fn()
.mockRejectedValueOnce(new Error('No element found matching selector: [data-opencli-ig-upload-index="0"]'))
.mockResolvedValueOnce(undefined);
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/STALE123/' },
]), { setFileInput });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'stale selector recovery',
});
expect(setFileInput).toHaveBeenCalledTimes(2);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/STALE123/',
},
]);
});
it('re-resolves the upload input when CDP loses the matched file-input node before setFileInput runs', async () => {
const imagePath = createTempImage('stale-node-id.jpg');
const setFileInput = vi.fn()
.mockRejectedValueOnce(new Error('{"code":-32000,"message":"Could not find node with given id"}'))
.mockResolvedValueOnce(undefined);
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/STALEID123/' },
]), { setFileInput });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'stale node id recovery',
});
expect(setFileInput).toHaveBeenCalledTimes(2);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/STALEID123/',
},
]);
});
it('retries opening the home composer instead of navigating to the broken /create/select route', async () => {
const imagePath = createTempImage('retry-composer.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: false },
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/FALLBACK123/' },
]));
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'retry composer',
});
const gotoCalls = page.goto.mock.calls.map((args) => String(args[0]));
expect(gotoCalls.every((url) => !url.includes('/create/select'))).toBe(true);
expect(gotoCalls.some((url) => url === 'https://www.instagram.com/')).toBe(true);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/FALLBACK123/',
},
]);
});
it('clicks Next twice when Instagram shows an intermediate preview step before the caption editor', async () => {
const imagePath = createTempImage('double-next.jpg');
let nextClicks = 0;
const evaluate = vi.fn(async (js) => {
if (js.includes('sharing') && js.includes('create new post'))
return { ok: false };
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasVisibleButtonInDialogs'))
return { state: 'preview', detail: 'Crop Back Next Select crop' };
if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
return { ok: nextClicks >= 2 };
}
if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
if (js.includes('"Share"'))
return { ok: true, label: 'Share' };
nextClicks += 1;
return { ok: true, label: 'Next' };
}
if (js.includes('ClipboardEvent') && js.includes('textarea'))
return { ok: true, mode: 'textarea' };
if (js.includes('readLexicalText'))
return { ok: true };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: 'https://www.instagram.com/p/DOUBLE123/' };
return { ok: true };
});
const page = createPageMock([], { evaluate });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'double next flow',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/DOUBLE123/',
},
]);
});
it('tries the next upload input when the first candidate never opens the preview', async () => {
const imagePath = createTempImage('second-input.jpg');
const setFileInput = vi.fn()
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined);
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]', '[data-opencli-ig-upload-index="1"]'] },
{ ok: true },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: false },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: 'https://www.instagram.com/p/SECOND123/' },
]), { setFileInput });
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'second input works',
});
expect(setFileInput).toHaveBeenNthCalledWith(1, [imagePath], '[data-opencli-ig-upload-index="0"]');
expect(setFileInput).toHaveBeenNthCalledWith(2, [imagePath], '[data-opencli-ig-upload-index="1"]');
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/SECOND123/',
},
]);
});
it('fails fast when Instagram reports that the post could not be shared', async () => {
const imagePath = createTempImage('share-failed.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: false, failed: true, url: '' },
]));
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: imagePath,
content: 'share should fail',
})).rejects.toThrow('Instagram post share failed');
});
it('keeps waiting across the full publish timeout window instead of fast-forwarding after 30 polls', async () => {
const imagePath = createTempImage('slow-share.jpg');
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
...Array.from({ length: 35 }, () => ({ ok: false, failed: false, settled: false, url: '' })),
{ ok: true, url: 'https://www.instagram.com/p/SLOWSHARE123/' },
]));
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'slow share eventually succeeds',
});
const waitCalls = page.wait.mock.calls.filter((args) => args[0]?.time === 1);
expect(waitCalls.length).toBeGreaterThanOrEqual(35);
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/SLOWSHARE123/',
},
]);
});
it('does not retry the upload flow after Share has already been clicked', async () => {
const imagePath = createTempImage('no-duplicate-retry.jpg');
const setFileInput = vi.fn().mockResolvedValue(undefined);
const page = createPageMock(withInitialDialogDismiss([
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
...Array.from({ length: 30 }, () => ({ ok: false, failed: false, url: '' })),
]), { setFileInput });
const cmd = getRegistry().get('instagram/post');
await expect(cmd.func(page, {
media: imagePath,
content: 'share observation stalled',
})).rejects.toThrow('Instagram post share confirmation did not appear');
expect(setFileInput).toHaveBeenCalledTimes(1);
});
it('recovers the latest post URL from the current logged-in profile when success does not navigate to /p/', async () => {
const imagePath = createTempImage('url-recovery.jpg');
const evaluate = vi.fn(async (js) => {
if (js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture')) {
return { data: [], errors: [] };
}
if (js.includes('fetch(') && js.includes('/api/v1/users/') && js.includes('X-IG-App-ID')) {
return js.includes('dynamic-runtime-app-id')
? { ok: true, username: 'tsezi_ray' }
: { ok: false };
}
if (js.includes('window.location?.pathname'))
return { ok: true };
if (js.includes('data-opencli-ig-upload-index'))
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
if (js.includes("dispatchEvent(new Event('input'"))
return { ok: true };
if (js.includes('const hasPreviewUi ='))
return { ok: true, state: 'preview' };
if (js.includes("scope === 'media'"))
return { ok: true, label: 'Next' };
if (js.includes("scope === 'caption'"))
return { ok: true, label: 'Share' };
if (js.includes('post shared') && js.includes('your post has been shared'))
return { ok: true, url: '' };
if (js.includes('const hrefs = Array.from(document.querySelectorAll(\'a[href*="/p/"]\'))')) {
const calls = evaluate.mock.calls.filter(([script]) => typeof script === 'string' && script.includes('const hrefs = Array.from(document.querySelectorAll(\'a[href*="/p/"]\'))')).length;
return calls === 1
? { ok: true, hrefs: ['/tsezi_ray/p/PINNED111/', '/tsezi_ray/p/OLD222/'] }
: { ok: true, hrefs: ['/tsezi_ray/p/PINNED111/', '/tsezi_ray/p/OLD222/', '/tsezi_ray/p/RECOVER123/'] };
}
if (js.includes('document.documentElement?.outerHTML')) {
return {
appId: 'dynamic-runtime-app-id',
csrfToken: 'csrf-token',
instagramAjax: 'dynamic-rollout',
};
}
return { ok: true };
});
const page = createPageMock([], {
evaluate,
getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '61236465677', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'url recovery',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/tsezi_ray/p/RECOVER123/',
},
]);
});
it('treats a closed composer as a successful share and falls back to URL recovery', async () => {
const imagePath = createTempImage('share-settled.jpg');
const page = createPageMock([
{ appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
{ ok: true, username: 'tsezi_ray' },
{ ok: true, hrefs: ['/p/OLD111/'] },
{ ok: false },
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: false, failed: false, settled: true, url: '' },
{ ok: false, failed: false, settled: true, url: '' },
{ ok: false, failed: false, settled: true, url: '' },
{ appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
{ ok: true, username: 'tsezi_ray' },
{ ok: true, hrefs: ['/p/OLD111/', '/p/RECOVER789/'] },
], {
getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '61236465677', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'share settled recovery',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/RECOVER789/',
},
]);
});
it('accepts standard /p/... profile links during URL recovery', async () => {
const imagePath = createTempImage('url-recovery-standard-shape.jpg');
const page = createPageMock([
{ appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
{ ok: true, username: 'tsezi_ray' },
{ ok: true, hrefs: ['/p/PINNED111/', '/p/OLD222/'] },
{ ok: false },
{ ok: true },
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
{ ok: true },
{ ok: true },
{ ok: false },
{ ok: true, label: 'Next' },
{ ok: true },
{ ok: true },
{ ok: true },
{ ok: true, label: 'Share' },
{ ok: true, url: '' },
{ appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
{ ok: true, username: 'tsezi_ray' },
{ ok: true, hrefs: ['/p/PINNED111/', '/p/OLD222/', '/p/RECOVER456/'] },
], {
getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '61236465677', domain: 'instagram.com' }]),
});
const cmd = getRegistry().get('instagram/post');
const result = await cmd.func(page, {
media: imagePath,
content: 'url recovery standard shape',
});
expect(result).toEqual([
{
status: '✅ Posted',
detail: 'Single image post shared successfully',
url: 'https://www.instagram.com/p/RECOVER456/',
},
]);
});
});