{"record":{"id":"ef5db60cb678876b","repo":"jackwener/OpenCLI","slug":"ctrip-cruise-cards-rendered-but-parser-did-not-fin","errorCode":null,"errorMessage":"Ctrip cruise cards rendered but parser did not find required itinerary anchors","messagePattern":"Ctrip cruise cards rendered but parser did not find required itinerary anchors","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/cruise.js","lineNumber":83,"sourceCode":"            await page.goto(searchUrl);\n            const portWait = await page.evaluate(WAIT_FOR_CRUISE_JS);\n            if (portWait === 'captcha') {\n                throw new AuthRequiredError('cruise.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');\n            }\n            if (portWait === 'empty') {\n                throw new EmptyResultError('ctrip cruise', `No cruises currently departing \"${port}\"`);\n            }\n            if (portWait !== 'content') {\n                throw new CommandExecutionError(`Ctrip cruise port page did not render (state=${String(portWait)})`);\n            }\n        }\n\n        const raw = await page.evaluate(buildCruiseExtractJs());\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Ctrip cruise DOM extraction returned malformed rows');\n        }\n        if (raw.length === 0) {\n            throw new CommandExecutionError('Ctrip cruise cards rendered but parser did not find required itinerary anchors');\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            title: r.title,\n            star: r.star,\n            boarding: r.boarding,\n            sailingDate: r.sailingDate,\n            tags: r.tags,\n            price: r.price,\n            url: searchUrl,\n        }));\n    },\n});\n","sourceCodeStart":65,"sourceCodeEnd":97,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/cruise.js#L65-L97","documentation":"CommandExecutionError thrown when the extraction script returns a well-formed but EMPTY array even though the page rendered (wait state was 'content'). The CLI distinguishes this from EmptyResultError: cards are visibly on the page, but buildCruiseExtractJs could not find the required itinerary anchors (e.g. links/fields inside .route_info cards it needs to build rows). This almost always means Ctrip changed its card markup so the parser's selectors no longer match.","triggerScenarios":"Calling `ctrip cruise <port>` where the page passed the content probe but the extraction script's required itinerary anchor selectors (inside .route_info cards) matched zero elements — markup redesign, renamed classes, or content moved behind lazy-loaded subcomponents after the probe ran.","commonSituations":"Ctrip front-end redeploy changing .route_info internals (A/B tests, redesigns); CLI version older than a recent site markup change; region-specific page variants (overseas ports) rendered with slightly different DOM; partial lazy-load so cards exist but their anchors haven't attached yet.","solutions":["Open the port results page in a browser DevTools and diff the actual card markup against the selectors in buildCruiseExtractJs (clis/ctrip/utils.js)","Update/fix the extraction selectors in clis/ctrip/utils.js to match the current .route_info structure","Upgrade the opencli package in case a newer version already tracks the new Ctrip markup","Add a short settle delay/scroll before extraction if anchors attach late during lazy hydration"],"exampleFix":"// before (utils.js): anchor selector outdated\nconst link = card.querySelector('a.itinerary-link');\nif (!link) return null;\n// after: match new markup (verify in DevTools)\nconst link = card.querySelector('a[href*=\"/cruise/\"], .route_title a');\nif (!link) return null;","handlingStrategy":"validation","validationCode":"// Validate the required anchors still exist in the live DOM before full extraction\nconst anchorsOk = await page.evaluate(() =>\n  document.querySelectorAll('.route_info a').length > 0\n);\nif (!anchorsOk) console.error('Ctrip card markup changed; update buildCruiseExtractJs selectors');","typeGuard":"function isParserMissError(e) {\n  return e instanceof Error && e.name === 'CommandExecutionError' && /itinerary anchors/.test(e.message);\n}","tryCatchPattern":"try {\n  return await run(['ctrip', 'cruise', port]);\n} catch (e) {\n  if (isParserMissError(e)) {\n    console.error('Ctrip markup likely changed — inspect .route_info in DevTools and update selectors');\n    throw e;\n  }\n  throw e;\n}","preventionTips":["Add a smoke test that runs the cruise command weekly to detect selector drift early","Diff .route_info card markup against extractor selectors after any Ctrip UI change notice","Keep opencli updated — fixes for markup changes land there first","Add a settle delay before extraction if anchors attach during late hydration"],"tags":["parser","selector-drift","markup-change","ctrip","dom-extraction"],"backgroundTag":"selector-drift-site-markup-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}