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
1648 lines
76 KiB
JavaScript
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/',
|
|
},
|
|
]);
|
|
});
|
|
});
|