← back to Marketing Command Center
demo/record-tiktok-demo.mjs
91 lines
// TikTok App-Review demo recorder for the Marketing Command Center.
// Records the end-to-end integration flow (Channels → Connect state → Composer →
// pick our own video → caption → select TikTok → Publish SELF_ONLY → Outbox) as a
// video, for the "complete end-to-end flow" upload TikTok App Review requires.
//
// Prereq: TikTok must be CONNECTED in the MCC (sandbox keys + one Connect) so the
// composer's TikTok target is enabled. Run: node demo/record-tiktok-demo.mjs
// Output: demo/out/<video>.webm → convert to mp4 (≤50MB) with ffmpeg (see README below).
import { chromium } from 'playwright';
import fs from 'node:fs';
import path from 'node:path';
const ROOT = 'https://marketing.designerwallcoverings.com';
const envTxt = fs.readFileSync(new URL('../.env', import.meta.url), 'utf8');
const g = (k) => (envTxt.match(new RegExp('^' + k + '=(.*)$', 'm'))?.[1] || '').replace(/^['"]|['"]$/g, '');
const USER = g('MCC_USER') || 'admin', PASS = g('MCC_PASS');
const OUT = path.join(path.dirname(new URL(import.meta.url).pathname), 'out');
fs.mkdirSync(OUT, { recursive: true });
const pause = (ms) => new Promise(r => setTimeout(r, ms));
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
httpCredentials: { username: USER, password: PASS },
viewport: { width: 1366, height: 900 },
recordVideo: { dir: OUT, size: { width: 1366, height: 900 } },
});
const page = await ctx.newPage();
async function clickByText(re, timeout = 8000) {
const el = page.getByText(re).first();
await el.waitFor({ state: 'visible', timeout }).catch(() => {});
await el.click({ timeout }).catch(() => {});
}
try {
// 1. Dashboard
await page.goto(ROOT, { waitUntil: 'networkidle' });
await pause(2500);
// 2. Channels panel — show TikTok connected
await clickByText(/channels/i);
await pause(3500);
// scroll the TikTok card into view if present
await page.getByText(/tiktok/i).first().scrollIntoViewIfNeeded().catch(() => {});
await pause(3000);
// 3. Composer — pick one of our own videos
await clickByText(/composer/i);
await pause(3000);
// click first media thumbnail in the source grid
const thumb = page.locator('.cmp-thumb, #cmp-srcgrid img, #ch-media').first();
await thumb.click({ timeout: 6000 }).catch(() => {});
await pause(2000);
// 4. Caption
const cap = page.locator('#cmp-caption, #ch-caption').first();
await cap.click().catch(() => {});
await cap.fill('New from Designer Wallcoverings — statement wallcoverings for the modern interior. #designerwallcoverings').catch(() => {});
await pause(2500);
// 5. Select TikTok target (enabled only when connected)
const tt = page.locator('input[data-ch="tiktok"]').first();
await tt.check({ timeout: 5000 }).catch(() => {});
await pause(2000);
// 6. Publish (SELF_ONLY in sandbox). Leave dry-run OFF so it posts to the sandbox account.
const pub = page.locator('#cmp-publish, #ch-publish').first();
await pub.scrollIntoViewIfNeeded().catch(() => {});
await pause(1000);
await pub.click({ timeout: 6000 }).catch(() => {});
await pause(6000); // let the post fire + result render
// 7. Result / outbox
await page.getByText(/posted|outbox|staged/i).first().scrollIntoViewIfNeeded().catch(() => {});
await pause(4000);
} catch (e) {
console.error('recorder error:', e.message);
} finally {
await ctx.close(); // finalizes the video file
await browser.close();
const vid = fs.readdirSync(OUT).filter(f => f.endsWith('.webm')).map(f => path.join(OUT, f)).sort().pop();
console.log('VIDEO:', vid || '(none)');
}
/*
Convert + trim to an mp4 the App-Review upload accepts (mp4/mov, ≤50MB) and drop in ~/Downloads:
ffmpeg -y -i demo/out/<video>.webm -vf "scale=1366:-2,fps=30" -c:v libx264 -pix_fmt yuv420p \
-movflags +faststart -crf 24 ~/Downloads/dw-tiktok-integration-demo.mp4
Optionally add a caption/title card and voiceover before upload.
*/