[object Object]

← back to Marketing Command Center

TK-10519: safe no-publish TikTok integration demo video (recorder + mp4) for portal Save

d56a62cc748a48ec7e5380c3619dbddf507fd5ac · 2026-08-31 12:02:07 -0700 · Steve Abrams

Files touched

Diff

commit d56a62cc748a48ec7e5380c3619dbddf507fd5ac
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 12:02:07 2026 -0700

    TK-10519: safe no-publish TikTok integration demo video (recorder + mp4) for portal Save
---
 .gitignore                          |   1 +
 demo/dw-tiktok-integration-demo.mp4 | Bin 0 -> 507868 bytes
 demo/record-tiktok-demo-safe.mjs    |  78 ++++++++++++++++++++++++++++++++++++
 3 files changed, 79 insertions(+)

diff --git a/.gitignore b/.gitignore
index 10f5a59..8e73fdb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,3 +80,4 @@ public/amp-assets/*
 !public/amp-assets/.gitkeep
 data/vendor-amplify-assets/
 data/vendor-amplify-assets.json
+demo/out/
diff --git a/demo/dw-tiktok-integration-demo.mp4 b/demo/dw-tiktok-integration-demo.mp4
new file mode 100644
index 0000000..c4a38ec
Binary files /dev/null and b/demo/dw-tiktok-integration-demo.mp4 differ
diff --git a/demo/record-tiktok-demo-safe.mjs b/demo/record-tiktok-demo-safe.mjs
new file mode 100644
index 0000000..9283263
--- /dev/null
+++ b/demo/record-tiktok-demo-safe.mjs
@@ -0,0 +1,78 @@
+// SAFE TikTok integration-flow demo — drives the REAL Quick Post wizard through
+// Picture → Words → Where(channels), screenshotting each true state. It STOPS at the
+// channel-selection step (never selects a channel, never reaches the Check/publish
+// step), so NOTHING can be sent to any connected account (FB/IG/TikTok). Frames →
+// demo/out/frame-*.png; stitched to mp4 by the system ffmpeg. TK-10519.
+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.rmSync(OUT, { recursive: true, force: true }); fs.mkdirSync(OUT, { recursive: true });
+const pause = (ms) => new Promise(r => setTimeout(r, ms));
+
+const browser = await chromium.launch({ headless: true, channel: 'chrome' });
+const ctx = await browser.newContext({
+  httpCredentials: { username: USER, password: PASS },
+  viewport: { width: 1366, height: 900 },
+});
+const page = await ctx.newPage();
+
+let n = 0;
+async function banner(text) {
+  await page.evaluate((t) => {
+    let b = document.getElementById('__demo_banner__');
+    if (!b) {
+      b = document.createElement('div'); b.id = '__demo_banner__';
+      b.style.cssText = 'position:fixed;left:0;right:0;bottom:0;z-index:2147483647;background:rgba(17,17,17,.93);color:#fff;font:600 22px/1.45 -apple-system,Segoe UI,sans-serif;padding:18px 28px;text-align:center;letter-spacing:.2px';
+      document.body.appendChild(b);
+    }
+    b.textContent = t;
+  }, text).catch(() => {});
+}
+async function shot(label, holdFrames = 4) {
+  if (label) await banner(label);
+  await pause(700);
+  for (let k = 0; k < holdFrames; k++) {
+    await page.screenshot({ path: path.join(OUT, `frame-${String(n++).padStart(3, '0')}.png`) }).catch(() => {});
+  }
+}
+const next = async () => { await page.locator('#kp-next').click({ timeout: 6000 }).catch(() => {}); await pause(1400); };
+
+try {
+  await page.goto(ROOT, { waitUntil: 'domcontentloaded', timeout: 45000 }); await pause(3800);
+  await shot('Designer Wallcoverings — Marketing Command Center · posting to TikTok', 4);
+
+  // STEP 1 — Picture: choose one of OUR OWN DW pictures/videos (the content the app posts).
+  await page.locator('.kp-asset').first().click({ timeout: 8000 }).catch(() => {});
+  await shot('Step 1 · Picture — the creator picks one of their own DW pictures/videos', 4);
+  await next();
+
+  // STEP 2 — Music (optional). Just show it and continue.
+  await shot('Step 2 · Music — optional brand-safe track', 3);
+  await next();
+
+  // STEP 3 — Words: the caption sent with the media via TikTok video.upload.
+  await page.evaluate(() => {
+    const c = document.querySelector('#kp-caption');
+    if (c) { c.value = 'New from Designer Wallcoverings — statement wallcoverings for the modern interior. #designerwallcoverings';
+      c.dispatchEvent(new Event('input', { bubbles: true })); }
+  });
+  await shot('Step 3 · Words — a caption is written (sent with the video via video.upload)', 4);
+  await next();
+
+  // STEP 4 — Where: the channel picker. TikTok appears here as a publish target.
+  await pause(1600); // channels fetch
+  await page.locator('#kp-channels .kp-channel[data-channel="tiktok"]').first().scrollIntoViewIfNeeded().catch(() => {});
+  await shot('Step 4 · Where — TikTok is offered as a publish target (Login Kit + Content Posting API)', 5);
+  await shot('This app requests user.info.basic + video.upload only — no Direct Post. Posts are SELF_ONLY in sandbox. [demo ends before publishing]', 5);
+} catch (e) {
+  console.error('recorder error:', e.message);
+} finally {
+  await ctx.close(); await browser.close();
+  console.log('FRAMES:', fs.readdirSync(OUT).filter(f => f.startsWith('frame-')).length, 'in', OUT);
+}

← 6bd3d5c auto-data-snapshot: 2026-08-31T11:55:57 (9 data files) — dem  ·  back to Marketing Command Center  ·  vendors: add LinkedIn vendor→company mapping + public OG har 6ccae80 →