Back to Repositories

Testing Node Manifest API Implementation in GatsbyJS

This integration test suite validates the Node Manifest API functionality in GatsbyJS, focusing on how node manifests are created, managed, and integrated with page creation and query tracking.

Test Coverage Overview

The test suite provides comprehensive coverage of Node Manifest API functionality across different scenarios.

  • Tests manifest creation with ownerNodeId in createPage
  • Validates manifest generation through pageContext matching
  • Verifies filesystem routes and connection list queries
  • Tests manifest file limits and cleanup
  • Checks updatedAt timestamp handling

Implementation Analysis

The testing approach uses Jest’s describe/it blocks to organize related test cases with proper setup and teardown.

Key patterns include:
  • Process spawning for Gatsby development and build modes
  • File system operations for manifest verification
  • Async/await patterns for file reading and process management
  • Environment variable configuration for test conditions

Technical Details

Testing tools and configuration:

  • Jest as the primary testing framework
  • Custom utility functions for Gatsby process management
  • File system utilities (fs-extra, rimraf)
  • Environment variables for test configuration
  • Port configuration (8010) for development server

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through careful organization and thorough validation.

  • Proper test isolation with beforeAll/afterAll hooks
  • Comprehensive error handling and cleanup
  • Clear test case descriptions
  • Modular test setup with shared utilities
  • Proper async/await usage for asynchronous operations

gatsbyjs/gatsby

integration-tests/node-manifest/__tests__/create-node-manifest.test.js

            
// Must run `gatsby-dev --packages gatsby to get latest gatsby changes before running the tests
jest.setTimeout(100000)
const DEFAULT_MAX_DAYS_OLD = 30

const {
  spawnGatsbyProcess,
  runGatsbyClean,
} = require(`../utils/get-gatsby-process`)
const urling = require(`urling`)
const rimraf = require(`rimraf`)
const path = require(`path`)
const fs = require(`fs-extra`)

const gatsbyCommandName = process.env.GATSBY_COMMAND_NAME || `develop`

const manifestDir = path.join(
  process.cwd(),
  `public`,
  `__node-manifests`,
  `default-site-plugin`
)

const pageDataDir = path.join(process.cwd(), `public`, `page-data`)

const createManifestId = nodeId => `${gatsbyCommandName}-${nodeId}`

const cleanNodeManifests = async () => {
  console.log(`removing any lingering node manifest files at ${manifestDir}`)
  return new Promise(resolve => rimraf(manifestDir, resolve))
}

const getManifestContents = async nodeId =>
  await fs.readJSON(path.join(manifestDir, `${createManifestId(nodeId)}.json`))

const pageDataContents = async pagePath =>
  await fs.readJSON(path.join(pageDataDir, pagePath, `page-data.json`))

const port = 8010

// see gatsby-node.js for where createNodeManifest was called
// and for the corresponding pages that were created with createPage
describe(`Node Manifest API in "gatsby ${gatsbyCommandName}"`, () => {
  let gatsbyProcess

  beforeAll(async () => {
    await cleanNodeManifests()

    gatsbyProcess = spawnGatsbyProcess(gatsbyCommandName, {
      PORT: port,
    })

    if (gatsbyCommandName === `develop`) {
      // wait for localhost
      return urling(`http://localhost:${port}`)
    } else if (gatsbyCommandName === `build`) {
      // for gatsby build wait for the process to exit
      return gatsbyProcess
    }
  })

  afterAll(() => {
    return new Promise(resolve => {
      if (
        !gatsbyProcess ||
        gatsbyProcess.killed ||
        gatsbyProcess.exitCode !== null
      ) {
        return resolve()
      }

      gatsbyProcess.on(`exit`, () => {
        setImmediate(() => {
          resolve()
        })
      })

      gatsbyProcess.kill()
    })
  })

  it(`Creates an accurate node manifest when using the ownerNodeId argument in createPage`, async () => {
    const manifestFileContents = await getManifestContents(1)

    expect(manifestFileContents.node.id).toBe(`1`)
    expect(manifestFileContents.page.path).toBe(`/one/`)
    expect(manifestFileContents.foundPageBy).toBe(`ownerNodeId`)
  })

  it(`Creates an accurate node manifest when ownerNodeId isn't present but there's a matching "id" in pageContext`, async () => {
    const manifestFileContents = await getManifestContents(2)

    expect(manifestFileContents.node.id).toBe(`2`)
    expect(manifestFileContents.page.path).toBe(`/two/`)
    expect(manifestFileContents.foundPageBy).toBe(`context.id`)
  })

  it(`Creates an accurate node manifest when ownerNodeId isn't present but there's a matching "slug" in pageContext`, async () => {
    const manifestFileContents = await getManifestContents(5)

    expect(manifestFileContents.node.id).toBe(`5`)
    expect(manifestFileContents.page.path).toBe(`/slug-test-path/`)
    expect(manifestFileContents.foundPageBy).toBe(`context.slug`)
  })

  if (gatsbyCommandName === `build`) {
    // this doesn't work in gatsby develop since query tracking
    // only runs when visiting a page in browser.
    it(`Creates a node manifest from the first tracked page it finds in query tracking`, async () => {
      const manifestFileContents = await getManifestContents(3)

      expect(manifestFileContents.node.id).toBe(`3`)
      expect(
        [`/three/`, `/three-alternative/`].includes(
          manifestFileContents.page.path
        )
      ).toBe(true)
      expect(manifestFileContents.foundPageBy).toBe(`queryTracking`)
    })

    // this doesn't work in gatsby develop since page-data.json files aren't written out
    it(`Adds the correct manifestId to the pageData.json digest`, async () => {
      const nodeId = `1`
      const path = `one`
      const pageData = await pageDataContents(path)

      expect(pageData.manifestId).toEqual(createManifestId(nodeId))
    })
  }

  it(`Creates a node manifest with a null page path when createNodeManifest is called but a page is not created for the provided node in the Gatsby site`, async () => {
    const manifestFileContents = await getManifestContents(4)

    expect(manifestFileContents.node.id).toBe(`4`)
    expect(manifestFileContents.page.path).toBe(null)
    expect(manifestFileContents.foundPageBy).toBe(`none`)
  })

  it(`Creates a Node manifest for filesystem routes`, async () => {
    const manifestFileContents = await getManifestContents(`filesystem-1`)

    expect(manifestFileContents.node.id).toBe(`filesystem-1`)
    expect(manifestFileContents.page.path).toBe(`/filesystem-1/`)
    expect(manifestFileContents.foundPageBy).toBe(`filesystem-route-api`)
  })

  it(`Creates a manifest for the node when updatedAt is included and is within ${DEFAULT_MAX_DAYS_OLD} days`, async () => {
    const recentlyUpdatedNodeId = `updatedAt-1`
    const staleNodeId = `updatedAt-2`
    const recentlyUpdatedNodeManifest = await getManifestContents(
      recentlyUpdatedNodeId
    )

    try {
      await getManifestContents(staleNodeId)
      throw new Error()
    } catch (e) {
      expect(e.message).toContain(`no such file or directory`)
      expect(e.message).toContain(createManifestId(staleNodeId))
    }

    expect(recentlyUpdatedNodeManifest.node.id).toBe(recentlyUpdatedNodeId)
  })

  it(`Creates a correct node manifest for nodes in connection list queries`, async () => {
    const manifestFileContents1 = await getManifestContents(
      `connection-list-query-node`
    )

    expect(manifestFileContents1.node.id).toBe(`connection-list-query-node`)
    expect(manifestFileContents1.page.path).toBe(`/connection-list-query-page/`)

    const manifestFileContents2 = await getManifestContents(
      `connection-list-query-node-2`
    )

    expect(manifestFileContents2.node.id).toBe(`connection-list-query-node-2`)
    expect(manifestFileContents2.page.path).toBe(`/connection-list-query-page/`)
  })

  it(`Creates a correct node manifest for nodes in connection list queries using staticQuery()`, async () => {
    const manifestFileContents1 = await getManifestContents(
      `static-query-list-query-node`
    )

    expect(manifestFileContents1.node.id).toBe(`static-query-list-query-node`)
    expect(manifestFileContents1.page.path).toBe(`/static-query-list-query/`)

    const manifestFileContents2 = await getManifestContents(
      `static-query-list-query-node-2`
    )

    expect(manifestFileContents2.node.id).toBe(`static-query-list-query-node-2`)
    expect(manifestFileContents2.page.path).toBe(`/static-query-list-query/`)
  })
})

describe(`Node Manifest API in "gatsby ${gatsbyCommandName}"`, () => {
  let gatsbyProcess

  beforeEach(async () => {
    await runGatsbyClean()

    gatsbyProcess = spawnGatsbyProcess(gatsbyCommandName, {
      DUMMY_NODE_MANIFEST_COUNT: 700,
      NODE_MANIFEST_FILE_LIMIT: 500,
      PORT: port,
    })

    if (gatsbyCommandName === `develop`) {
      // wait for localhost
      await urling(`http://localhost:${port}`)
    } else if (gatsbyCommandName === `build`) {
      // for gatsby build wait for the process to exit
      return gatsbyProcess
    }
  })

  afterAll(() => {
    return new Promise(resolve => {
      if (
        !gatsbyProcess ||
        gatsbyProcess.killed ||
        gatsbyProcess.exitCode !== null
      ) {
        return resolve()
      }

      gatsbyProcess.on(`exit`, () => {
        setImmediate(() => {
          resolve()
        })
      })

      gatsbyProcess.kill()
    })
  })

  it(`Limits the number of node manifest files written to disk to 500`, async () => {
    const nodeManifestFiles = fs.readdirSync(manifestDir)
    expect(nodeManifestFiles).toHaveLength(500)
  })
})