docs-v2/cypress/e2e/content/api-reference.cy.js

375 lines
12 KiB
JavaScript

/// <reference types="cypress" />
/**
* API Reference E2E Tests
*
* Validates Hugo-native API reference rendering:
* - Sidebar navigation
* - Format selector ("Copy page/section for AI")
* - OpenAPI spec download buttons
* - In-page TOC with operation links
* - Operations, code samples, and related links
*
* Coverage: 1 tag page + 1 conceptual page per product.
*
* Run:
* node cypress/support/run-e2e-specs.js \
* --spec "cypress/e2e/content/api-reference.cy.js" \
* content/influxdb3/core/reference/api/_index.md
*/
const SENTINEL = 'content/influxdb3/core/api/write-data/_index.md';
const FORMAT_BTN =
'.api-header-actions [data-component="format-selector"] .format-selector__button';
before(() => {
cy.task('readFile', SENTINEL).then((content) => {
if (content) return;
cy.log('Generating API content from OpenAPI specs...');
cy.exec('node api-docs/scripts/dist/post-process-specs.js', {
timeout: 30000,
});
cy.exec(
'node api-docs/scripts/dist/generate-openapi-articles.js --skip-fetch',
{ timeout: 120000 }
);
cy.request({
url: '/influxdb3/core/api/write-data/',
retryOnStatusCodeFailure: true,
timeout: 60000,
});
});
});
// Single-spec products: flat tag list in sidebar
const products = [
{
name: 'Core',
base: '/influxdb3/core/api',
dataPath: 'data/article_data/influxdb3/core/api/articles.yml',
},
{
name: 'Enterprise',
base: '/influxdb3/enterprise/api',
dataPath: 'data/article_data/influxdb3/enterprise/api/articles.yml',
},
{
name: 'Cloud Serverless',
base: '/influxdb3/cloud-serverless/api',
dataPath: 'data/article_data/influxdb3/cloud-serverless/api/articles.yml',
},
{
name: 'Cloud',
base: '/influxdb/cloud/api',
dataPath: 'data/article_data/influxdb/cloud/api/articles.yml',
},
{
name: 'v2',
base: '/influxdb/v2/api',
dataPath: 'data/article_data/influxdb/v2/api/articles.yml',
},
];
// Multi-spec products: nested Data API / Management API sub-sections
const multiSpecProducts = [
{
name: 'Cloud Dedicated',
base: '/influxdb3/cloud-dedicated/api',
specs: [
{
slug: 'data-api',
label: 'Data API',
dataPath:
'data/article_data/influxdb3/cloud-dedicated/data-api/articles.yml',
},
{
slug: 'management-api',
label: 'Management API',
dataPath:
'data/article_data/influxdb3/cloud-dedicated/management-api/articles.yml',
},
],
},
{
name: 'Clustered',
base: '/influxdb3/clustered/api',
specs: [
{
slug: 'data-api',
label: 'Data API',
dataPath:
'data/article_data/influxdb3/clustered/data-api/articles.yml',
},
{
slug: 'management-api',
label: 'Management API',
dataPath:
'data/article_data/influxdb3/clustered/management-api/articles.yml',
},
],
},
];
// ── Sidebar nav ──────────────────────────────────────────────────────
describe('API sidebar navigation', () => {
products.forEach(({ name, base, dataPath }) => {
it(`${name}: sidebar contains every tag from articles.yml and All endpoints`, () => {
cy.task('readArticleData', dataPath).then((tags) => {
expect(tags, `article data loaded from ${dataPath}`).to.be.an('array').and.have.length.at.least(1);
cy.visit(`${base}/`);
// Locate the children list nested under the API section link
cy.get('#nav-tree a')
.filter(`[href="${base}/"]`)
.parents('li')
.first()
.find('ul.children')
.as('apiNav');
// Every tag in the data file must appear as a nav link
tags.forEach((tag) => {
cy.get('@apiNav')
.find('li.nav-item a')
.then(($links) => {
const texts = [...$links].map((el) => el.textContent.trim());
expect(texts, `sidebar is missing tag "${tag}"`).to.include(tag);
});
});
// "All endpoints" must be the last nav item
cy.get('@apiNav')
.find('li.nav-item a')
.last()
.should('contain', 'All endpoints')
.and('have.attr', 'href', `${base}/all-endpoints/`);
});
});
});
});
// ── Multi-spec sidebar nav ────────────────────────────────────────────
describe('API sidebar navigation (multi-spec)', () => {
multiSpecProducts.forEach(({ name, base, specs }) => {
it(`${name}: sidebar shows nested sub-sections for each spec`, () => {
cy.visit(`${base}/`);
// Locate the children list nested under the API section link
cy.get('#nav-tree a')
.filter(`[href="${base}/"]`)
.parents('li')
.first()
.find('ul.children')
.as('apiNav');
specs.forEach(({ slug, label, dataPath }) => {
cy.task('readArticleData', dataPath).then((tags) => {
expect(
tags,
`article data loaded from ${dataPath}`
)
.to.be.an('array')
.and.have.length.at.least(1);
// Sub-section item for this spec must exist
cy.get('@apiNav')
.find(`li.nav-item`)
.filter(`:contains("${label}")`)
.as('specSection');
cy.get('@specSection').should('exist');
// Sub-section must have a children list with every tag
cy.get('@specSection')
.find('ul.children li.nav-item a')
.then(($links) => {
const texts = [...$links].map((el) => el.textContent.trim());
tags.forEach((tag) => {
expect(
texts,
`spec section "${label}" is missing tag "${tag}"`
).to.include(tag);
});
});
// Each spec sub-section must have its own "All endpoints" link
cy.get('@specSection')
.find('ul.children li.nav-item a')
.last()
.should('contain', 'All endpoints')
.and(
'have.attr',
'href',
`${base}/${slug}/all-endpoints/`
);
});
});
});
});
});
// ── Tag pages ────────────────────────────────────────────────────────
describe('API tag pages', () => {
products.forEach(({ name, base }) => {
it(`${name}: sidebar, format selector, download, TOC, operations`, () => {
cy.visit(`${base}/write-data/`);
// Sidebar link to product home
cy.get('#nav-tree a')
.first()
.should('have.attr', 'href')
.and('match', /^\/[^/]+\/[^/]+\/$/);
// Format selector
cy.get(FORMAT_BTN).should('contain', 'Copy page for AI');
// Download button
cy.get('.api-header-actions .api-spec-download')
.should('have.attr', 'href')
.and('match', /^\/openapi\/.+\.yml$/);
cy.get('.api-header-actions .api-spec-download').should(
'have.attr',
'download'
);
// In-page TOC
cy.get('.api-toc').should('exist');
cy.get('.api-toc-link').should('have.length.at.least', 1);
cy.get('.api-toc-link .api-method').should('have.length.at.least', 1);
// Operations
cy.get('.api-operation').should('have.length.at.least', 1);
cy.get('.api-operation')
.first()
.within(() => {
cy.get('.api-method').should('exist');
cy.get('.api-path').should('exist');
});
// Code sample
cy.get('.api-code-sample').should('have.length.at.least', 1);
cy.get('.api-code-block code')
.first()
.invoke('text')
.should('match', /curl/);
// Related links
cy.get('.related ul li a').should('have.length.at.least', 1);
});
});
});
// ── Conceptual pages (x-traitTag) ────────────────────────────────────
describe('API conceptual pages', () => {
products.forEach(({ name, base, dataPath }) => {
it(`${name}: all conceptual pages have content`, () => {
cy.task('readConceptualTags', dataPath).then((conceptualPages) => {
expect(
conceptualPages,
`conceptual pages loaded from ${dataPath}`
)
.to.be.an('array')
.and.have.length.at.least(1);
conceptualPages.forEach(({ tag, slug }) => {
cy.visit(`${base}/${slug}/`);
cy.get(FORMAT_BTN).should('contain', 'Copy page for AI');
cy.get('.api-conceptual-content')
.should('exist')
.invoke('text')
.then((text) => {
expect(
text.trim().length,
`"${tag}" conceptual page must have at least 50 characters of content`
).to.be.at.least(50);
});
});
});
});
});
multiSpecProducts.forEach(({ name, base, specs }) => {
it(`${name}: all conceptual pages have content`, () => {
specs.forEach(({ slug: specSlug, label, dataPath }) => {
cy.task('readConceptualTags', dataPath).then((conceptualPages) => {
expect(
conceptualPages,
`conceptual pages loaded from ${dataPath}`
)
.to.be.an('array')
.and.have.length.at.least(1);
conceptualPages.forEach(({ tag, slug }) => {
cy.visit(`${base}/${specSlug}/${slug}/`);
cy.get(FORMAT_BTN).should('contain', 'Copy page for AI');
cy.get('.api-conceptual-content')
.should('exist')
.invoke('text')
.then((text) => {
expect(
text.trim().length,
`"${tag}" (${label}) conceptual page must have at least 50 characters of content`
).to.be.at.least(50);
});
});
});
});
});
});
});
// ── Section index pages: format selector, download, children ─────────
describe('API section pages', () => {
products.forEach(({ name, base }) => {
it(`${name}: format selector, download, and children`, () => {
cy.visit(`${base}/`);
// Format selector with section label
cy.get(FORMAT_BTN).should('contain', 'Copy section for AI');
// Download button (from specDownloadPath frontmatter)
cy.get('.api-header-actions .api-spec-download')
.should('have.attr', 'href')
.and('match', /^\/openapi\/.+\.yml$/);
// Tag pages listed as children
cy.get('.children-links h3 a').should('have.length.at.least', 3);
});
});
});
// ── All endpoints page ───────────────────────────────────────────────
describe('All endpoints page', () => {
it('Core: format selector, download, and operation cards', () => {
cy.visit('/influxdb3/core/api/all-endpoints/');
// Format selector
cy.get('.api-header-actions [data-component="format-selector"]').should(
'exist'
);
// Download button (inherited from parent section)
cy.get('.api-header-actions .api-spec-download')
.should('have.attr', 'href')
.and('match', /^\/openapi\/.+\.yml$/);
// Operation cards
cy.get('.api-operation-card').should('have.length.at.least', 3);
cy.get('.api-operation-card')
.first()
.should('have.attr', 'href')
.and('match', /\/api\/.*\/#operation\//);
});
});