Skip to content

Repository files navigation

gramene-primers

PCR/qPCR primer design for Gramene sites (SorghumBase first), as an embeddable React component plus a headless TypeScript client. It talks to the gramene-swagger /primers endpoints:

  • Design: Primer3 2.6.1 on a gene (± flanks), a spliced transcript (junction-spanning qPCR), a genomic region or a pasted sequence, optionally avoiding repeats.

  • Genome specificity: a Primer-BLAST-style check against the reference genome, and against its transcriptome in transcript mode.

  • Pan-genome coverage: the same check across every assembly of the species.

  • React 18 is a peer dependency; the package has no runtime dependencies.

  • ESM + CJS builds with TypeScript declarations; styles are injected at runtime and also shipped as gramene-primers/style.css.

Status: 0.2.0. The API endpoints are under development on the gramene-swagger primer-design branch. Check results follow check algorithm version 2.

Install

npm install gramene-primers react@^18.2 react-dom@^18.2

During development, link a tarball rather than npm link (a symlink would load a second React):

npm run pack:local                       # gramene-primers-0.2.0.tgz
cd ../gramene-search && npm install --no-save ../gramene-primers/gramene-primers-0.2.0.tgz && rm -rf .parcel-cache

npm run lint:pkg packs into a temporary directory, so it never deletes that tarball, whichever runs last.

PrimerDesigner

import { PrimerDesigner } from 'gramene-primers';

<PrimerDesigner
  key={gene._id}
  apiBase="https://data.sorghumbase.org/sorghum_v11"
  gene={gene}                      // Gramene gene doc; or geneId, systemName+region, sequence
  systemName={gene.system_name}
  modes={['gene', 'transcript', 'region', 'sequence', 'genotyping']}
  state={saved} onStateChange={save} persistSequence={false}
  geneHref={(id) => `?idList=${encodeURIComponent(id)}`}
/>;

Props

Prop Type Default Notes
apiBase string required Base URL including the swagger basePath, e.g. https://data.sorghumbase.org/sorghum_v11
client PrimersClient createPrimersClient({apiBase}) Supply your own (custom fetch, headers, a test double)
gene GrameneGene Enables Gene and Transcript modes and prefills Region
geneId string Without a doc; fetched with GET /genes?idList=
systemName string the gene's Reference genome; enables Region mode
region {region, start, end, strand?} the gene location Region prefill
sequence string Sequence-mode prefill
modes DesignerMode[] the four design modes Offered modes; a mode without its inputs is hidden. genotyping is opt-in: list it to offer KASP/AS-PCR design (it needs a genome)
genesInRegion GenesInRegion Gene models for the genotyping variant browser; without it the gene track is hidden
alleleFrequencies AlleleFrequencies Allele frequencies for listed variants; without it there is no frequency column. Must be the same assembly and release the primers API reads variants from
sequenceForRegion SequenceForRegion Reference sequence for the CAPS annotation; without it the CAPS column reads "unknown". Must be the same assembly and release the primers API reads variants from
enzymes RestrictionEnzyme[] COMMON_ENZYMES Restriction enzymes to consider — pass what the lab actually stocks
defaultMode, defaultParams Initial mode and Primer3 parameter overrides
state, onStateChange PrimerDesignerState uncontrolled Controlled, JSON-serializable {v: 1, …} state
persistSequence boolean true false keeps the pasted sequence out of emitted state (it survives only while mounted)
onDesign(res, req), onCheckUpdate(job), onError(err) err is a PrimersApiError
features {check, pangenome, export, map, genotyping} each true genotyping: false hides the mode even when it is listed in modes
geneHref(id, systemName?), onGeneClick(id, systemName?) Gene links in results; onGeneClick handles plain left clicks, modified clicks follow geneHref
geneLabel string gene name or id Used in export file names and FASTA headers
theme 'light' | 'dark' | 'auto' 'auto' auto follows prefers-color-scheme
className, style On the .gpr-root element
injectStyles boolean true See Styles
poll {initialDelayMs, maxDelayMs, factor} 1000, 10000, 1.5 Check polling

State, identity and restore

  • Identity is gene._id ?? geneId ?? systemName+region ?? hash(sequence). A change aborts the running design and polling and resets the state, unless the host passed a new state in the same render. Hosts should also pass key.
  • State holds inputs, selections and check.jobId, never responses or results. normalizeDesignerState reads saved state tolerantly (invalid fields are dropped or clamped).
  • Restore: designed: true re-runs the design once, but only when the restored inputs pass the same checks as the Design primers button. A sequence-mode state saved with persistSequence={false} has no sequence, so nothing is sent and the form waits for a paste.
  • Saved check: check.jobId is read once. A queued or running job is polled, a finished one rendered, and a 404 shows "Results expired — Re-run check" (never an automatic resubmit).
  • Preview template sends template_only. The server does not compare the product size ranges with the template length for a preview, so that rule blocks only Design primers.
  • Checks: results are matched to pairs by UPPERCASE primer sequence, not by rank. Check job ids are shared: when POST /primers/check answers with a job that already finished (status only, no results), the job is read once with GET.
  • Pasted FASTA: several records are joined into one template; the Sequence input notes it (server warning MULTIPLE_RECORDS).

Primer3 help

  • Settings: each Primer3 setting (size, Tm and GC ranges, Max Tm difference, pairs to return, product size ranges, every advanced parameter and the repeat-masking choice) has a ? button. It shows what the setting does and links to its tag in the Primer3 2.6.1 manual; one help text is open at a time.
  • Inputs and explain: the interval, junction and explain texts link to the Primer3 tags they map to.
  • Results: PairsTable ends with "About these columns", and the designer's results end with a credit naming the Primer3 version (engine.primer3) and its citation.
  • Manual links open in a new tab, so a design and a running check stay put.

Other components

Each takes theme, injectStyles, className and style (StyleProps). Used on their own they render a .gpr-root; inside a PrimerDesigner they share the designer's.

Component Main props
PairsTable pairs, template?, checkedRanks? + onCheckedChange? (check boxes), selectedRank? + onSelect?, check? (a job: verdict chips), submitted?, label?, caption?
TemplateMap template, pairs?, selectedRank?, onSelect?, target?, included?, excluded?, title?
SpecificityResults results, block ('genome' or 'transcriptome'), pairs?, job?, submitted?, systemName?, geneHref?, onGeneClick?, maxRows? (100)
PangenomeMatrix results (PangenomeResults), requestedGenomes? (pending rows), genomes? (display names), params? (results.params), pairLabels?, transcriptModelsOnly?, geneHref?, onGeneClick?, caption?; plus PangenomeLegend
GenomePicker genomes, mode, systemName?, selected? (null = all), onChange(list | undefined), disabled?, label?

Reading check results (algorithm version 2):

  • Amplifying products: a product counts only when each primer has at most max_amplifying_mismatches mismatches (default 3) and passes the Primer-BLAST 3′ rule. Other products are unlikely.
  • Unlikely products are listed with include_unlikely, and the table says why: "a primer has more than 3 mismatches" or "mismatches near the 3′ end". A pan-genome genome with only unlikely products is no_amplicon, and CellDetail names its closest product.
  • Approximate counts: mismatch counts taken from BLAST alignments (cDNA hits) are marked approximate.
  • Pan-genome caps: a genome whose search hit a site, candidate or re-alignment cap is truncated, and its status is a lower bound. The matrix marks that cell ⋯, the column header shows "N incomplete", a note explains the marker, and CellDetail says products may have been missed. The data is in summary.truncated and in the TSV truncated column.
  • Transcript mode: the genome block has no on-target product. Products inside the gene are genomic DNA products; the rest are off-targets.

Styles

  • Injection: the stylesheet is injected once per document as <style id="gramene-primers-styles"> at the start of <head>, so host rules of equal specificity win. PrimerDesigner and each standalone component inject it. Components rendered inside a PrimerDesigner never inject on their own; the designer's injectStyles decides.
  • CSP-strict pages (no 'unsafe-inline' in style-src): pass injectStyles={false} to every top-level component and import the file instead:
    import 'gramene-primers/style.css';   // dist/gramene-primers.css
  • ensureStylesInjected(target?) injects into document or a ShadowRoot. It returns true when it added the element and is safe outside browsers. STYLE_ELEMENT_ID and PRIMERS_CSS (the text) are exported too.
  • Scoping: every rule sits under .gpr-root, with gpr- class names and --gpr-* custom properties and no !important. A defensive reset beats the Bootstrap 4 reboot. Themes are .gpr-theme-light, .gpr-theme-dark and auto; the layout uses two columns at a container width of 960 px or more.
  • Resizable columns: in the two-column layout, drag the bar between the form and the results, or focus it and use the arrow keys (Shift for bigger steps), Home and End. Double-click resets the default. The form column stays at least 300 px and the results at least 360 px; the width is kept in state.view.formWidth.

mount (hosts without React)

import { mount, ensureStylesInjected } from 'gramene-primers';

const handle = mount('#primers', { apiBase, geneId: 'SORBI_3001G000200', onStateChange: save });
handle.update({ theme: 'dark' });   // merges props and re-renders
handle.unmount();                   // aborts design and polling requests and empties the element

// Inside a shadow root:
const shadow = host.attachShadow({ mode: 'open' });
ensureStylesInjected(shadow);
mount(shadow.appendChild(document.createElement('div')), { apiBase, gene, injectStyles: false });

mount(el, props) takes an element or a selector and throws when nothing matches. React and ReactDOM 18 must still be installed, since they are peer dependencies.

Headless API

import {
  createPrimersClient, buildDesignRequest, buildCheckRequest, initialDesignerState,
  matchCheckResults, estimateCheckCpu, pairsToTSV, isAbortError, PrimersApiError,
} from 'gramene-primers';

const client = createPrimersClient({ apiBase: 'https://data.sorghumbase.org/sorghum_v11' });

const gene = await client.getGene('SORBI_3001G000200');
const state = initialDesignerState({ gene, defaultMode: 'transcript' });
const design = await client.design(buildDesignRequest(state, { gene }));

const request = buildCheckRequest({
  mode: 'transcript', systemName: gene.system_name, geneId: gene._id,
  transcriptId: design.template.transcript_id, pairs: design.pairs.slice(0, 2),
});
const job = await client.runCheck(request, { onUpdate: (j) => console.log(j.status, j.progress) });
const byPair = matchCheckResults(design.pairs, job);   // matched by UPPERCASE primer sequence

Client

Method Endpoint Notes
design(req, {signal}) POST /primers/design 60 s timeout
listGenomes(systemName) GET /primers/genomes?system_name= memoized per genome, evicted on error
submitCheck(req) POST /primers/check created: true for 202, false for an existing job (200); the answer has status and progress only
getCheck(jobId) GET /primers/check/{job_id} the job document, with request and results
pollCheck(jobId, opts) resolves at done or error
runCheck(req, opts) submit (reading an already finished job once), then poll; resubmits once if the job expires
getGene(geneId) GET /genes?idList= null when unknown

Transport rules:

  • Every request sends Accept: application/json and uses cache: 'no-store', credentials: 'omit' and mode: 'cors'.
  • Path and query values are URI-encoded, and no other query parameters are ever added.

Failures reject with PrimersApiError {status, code, message, details, errors, retryAfterMs}:

Response code
Handler error {message, code, details} the server's code, e.g. UNKNOWN_GENE, BUSY, JOB_TOO_LARGE, INVALID_PARAMS
Swagger validator 400 {message, errors} VALIDATION; errors[] is the validator's list, and flattenValidationErrors(errors) returns the nested reasons (e.g. OBJECT_ADDITIONAL_PROPERTIES, PATTERN with the body path)
Non-JSON body (e.g. an HTML 502) HTTP_<status>
Network failure / client timeout NETWORK / TIMEOUT (status 0)

retryAfterMs comes from details.retry_after_s, then the Retry-After header. Aborted requests reject with the original AbortError (test with isAbortError).

Polling

Polling starts after 1 s and grows ×1.5 per poll up to 10 s:

  • It resets when progress.done changes and waits at least 2 s while the job is queued.
  • 503, other 5xx, NETWORK and TIMEOUT errors sleep retryAfterMs (or the current delay); polling gives up after 5 consecutive errors.
  • On 404 it throws UNKNOWN_JOB, unless resubmit is given, in which case it resubmits once.
  • A finished job from submitCheck (as initialJob, or after a resubmit) is read once with getCheck (needsJobDocument).
  • It pauses while the document is hidden and stops on signal abort.

All of these are tunable through PollOptions.

Check parameters

buildCheckRequest sends only the values that differ from these defaults; validateCheckParams mirrors the server.

Param Range Default Meaning
max_product_size 50–10,000 4000 Largest product called; raised to cover expected products (checkMaxProductSize)
ignore_mismatches 3–6 6 A primer site with this many mismatches is dropped
max_amplifying_mismatches 0–5 3 A product amplifies only when each primer has at most this many; must be below ignore_mismatches. When omitted, the server lowers the default to ignore_mismatches − 1 (effectiveMaxAmplifyingMismatches)
min_total_mismatches 0–6 2 With min_3p_mismatches: the Primer-BLAST 3′ rule for unlikely
min_3p_mismatches 1–5 2 Mismatches required inside the 3′ window
three_prime_window 3–10 5 Bases from the 3′ end
include_unlikely boolean false Also list unlikely products
repeat_site_threshold 1–100 5 More near-perfect sites than this flags a primer repetitive

A saved pan-genome genome list is narrowed to the genomes the check can search in its mode (a cDNA database in transcript mode). When none remain, buildCheckRequest throws CheckRequestError('NO_GENOMES').

Cost estimate

estimateCheckCpu mirrors the server's check/cost.js and is used for display and to disable Submit above 6000 CPU-s:

cpu_s = uniq_primers × [ ref_Gb × c(5) + (transcript ? 0.15 × c(5) : 0)
                         + PANGENOME_CPU_FACTOR × Σ_pan (genome_Gb | 0.15) × c(6)
                         + genome_tasks × REALIGN_CPU_S_PER_PRIMER_TASK ]
c = {5: 5.2, 6: 2.2, 7: 1.2} CPU-s per primer·Gb; PANGENOME_CPU_FACTOR = 2.0; REALIGN_CPU_S_PER_PRIMER_TASK = 0.6;
genome_tasks = 1 + (transcript ? 0 : pan-genome genomes)   (cDNA searches re-align from the BLAST alignment)
unknown genome sizes (reference or pan-genome) count as FALLBACK_GENOME_GB = 1 Gb
  • PANGENOME_CPU_FACTOR accounts for pan-genome genomes being searched by up to 8 concurrent blastn processes, which used about twice the single-thread CPU on the full sorghum panel. As a result, 20 primers against all 119 sorghum genomes exceed the limit.
  • cpu_s is rounded up, ignoring float noise below 1e-6 as the server does.
  • The result also reports realign_cpu_s and genome_tasks.

Helpers

Export Purpose
buildDesignRequest(state, ctx) Mode-specific body; only params that differ from the mode's server preset
buildCheckRequest(input) Pair ids P{rank+1}; expected only in gene/region modes; genomes narrowed to searchable ones and omitted when all are selected; throws CheckRequestError
availableGenomeNames(genomes, mode, systemName), defaultPangenomeGenomes Genomes a pan-genome check can search
isCheckablePrimer, pairCheckability, CHECK_LIMITS 15–36 nt ACGT primers, ≤ 10 pairs / 20 unique primers, products ≤ 10 kb
checkMaxProductSize(req) Mirrors the server raise of max_product_size to min(10000, ceil(1.2 × largest expected))
PRESETS, PRIMER3_DEFAULTS, CHECK_DEFAULTS, CHECK_PARAM_LIMITS Server presets pcr / qpcr and defaults
validateDesignParams, validateIntervals, validateCheckParams, cleanSequenceInput Inline validation mirroring the server (including max_size ≤ the smallest product size, and the FASTA records count)
EXPLAIN_HINTS, explainRows, summarizeExplain Primer3 2.6.1 explain labels → hints (shown on NO_PAIRS)
pairsToTSV, primersToFasta, ampliconsToFasta, offTargetsToTSV, pangenomeToTSV Exports; TSV cells are protected against spreadsheet formula injection
copyText, downloadText Clipboard with textarea fallback; Blob downloads
estimateCheckCpu, REALIGN_CPU_S_PER_PRIMER_TASK CPU estimate (limit 6000 CPU-s)
initialDesignerState, normalizeDesignerState, toPersistedState, designerIdentity Serializable v1 state and identity
matchCheckResults, unlikelyReason, unlikelyText Results by sequence; why a product is unlikely
summarizePangenome, truncatedGenomeCount, pangenomeRows, PANGENOME_STATUS_META Pan-genome matrix data (amplifies = single_perfect + single_mismatch + multiple; truncated is a flag, not a status)
revcomp, transcriptLayout, cdnaToGenomicBlocks, mismatchIndexes, formatGenomic IUPAC-aware, case-preserving coordinates (1-based, inclusive)
annotateVariants, capsCall, differentialSites, dcapsOpportunities, enzymeCounts CAPS / dCAPS annotation for a variant listing — see CAPS
alleleShare, minorAlleleFrequency, dedupePopulations, populationCounts, variantAllele Allele frequency — see Allele frequency
digestAmplicon, singleCutters, nonCutters, ampliconSeq Restriction sites in a predicted product — see Restriction sites on a product
genomicToTemplatePosition, variantOnTemplate Genomic coordinates onto a template — the inverse of templateGenomicPosition, skipping introns and complementing alleles on a minus strand
findSites, iupacMatcher, digestFragments, isResolvable, variantContext, verifyWindow The restriction-site engine underneath it
COMMON_ENZYMES, findEnzyme, enzymeSpecificity, isSixCutter The bundled enzyme panel

CAPS

A variant can often be typed by digesting an ordinary PCR product instead of by allele-specific priming: CAPS when the variant itself creates or destroys a restriction site, dCAPS when a deliberate mismatch in the primer creates one.

This is an annotation — a property of the sequence — not a third assay type. A genotyping set is three oligos by definition, which CAPS does not fit, so nothing about designing or ordering sets changes. The column simply tells you whether a cheaper assay is available for a variant before you commit to KASP.

It runs entirely in the browser and needs no new endpoint, but it does need reference sequence, which a variant listing does not carry. Supply sequenceForRegion and the column fills in; omit it and every row reads Unknown — never None, which would be a claim the client cannot make.

<PrimerDesigner
  apiBase={apiBase}
  gene={gene}
  modes={['gene', 'genotyping']}
  sequenceForRegion={async ({ system_name, region, start, end }, options) => {
    const r = await fetch(`${ensemblRest}/sequence/region/${species}/${region}:${start}-${end}?content-type=application/json`, options);
    return (await r.json()).seq;
  }}
/>

Headless, without the components:

import { annotateVariants, COMMON_ENZYMES } from 'gramene-primers';

const caps = annotateVariants(variants, windowSeq, windowStart);
caps.get(variant.key); // { verdict: 'caps' | 'dcaps' | 'none', sites, dcaps, unknown }

Worth knowing before you trust a verdict:

  • The sequence must match the variant source's release. A mismatched gene track looks wrong; mismatched sequence produces a confident, wrong enzyme call. Every window is therefore checked against the reference alleles the API reports, and one disagreement marks the whole window unknown.
  • Six-cutters are ranked first. Four-cutters discriminate far more variants but cut an amplicon too often to read on a gel, so they are reported and ranked below. enzymeSpecificity is what separates them — XmnI (GAANNNNTTC) is ten characters long and still a six-cutter.
  • dCAPS reports the opportunity, not the primer. It gives the enzyme, which side of the variant the mismatch sits on, how far away, and which base — not a designed primer, which needs the thermodynamics the server owns.
  • Methylation is not modelled. Digests are computed from sequence alone, so a Dam- or Dcm-sensitive enzyme may fail on DNA this annotation calls cuttable.
  • The panel is a default. Pass enzymes with what the lab stocks.

Recognition sequences and cut positions follow REBASE (Roberts et al., Nucleic Acids Res 43:D298, 2015).

Allele frequency

How common an allele is decides whether a variant is worth typing: a marker near fixation in the panel you mean to screen tells you nothing, however well its primers score. Supply alleleFrequencies and the variant table gains a sortable Frequency column, a Panel menu, and a Polymorphic only toggle that hides variants whose minor allele is carried by under 5% of the panel. Without the callback there is no column.

The library decides how much to ask for and how often; the host only makes the request. Frequencies are fetched in batches of 200 ids, two in flight, stopping after 600 variants with a note — a listing can run to thousands, and filling every row would mean minutes of requests for a table nobody reads to the end. Answers are kept by variant id, so filtering and sorting cost nothing.

<PrimerDesigner
  apiBase={apiBase}
  gene={gene}
  modes={['gene', 'genotyping']}
  alleleFrequencies={async ({ system_name, ids }, options) => {
    const r = await fetch(`${ensemblRest}/variation/${system_name}?pops=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ids }),
      signal: options?.signal,
    });
    const body = await r.json();
    return Object.fromEntries(
      ids.filter((id) => body[id]).map((id) => [id, body[id].populations.map((p) => ({
        population: p.population, allele: p.allele, frequency: p.frequency, count: p.allele_count ?? null,
      }))]),
    );
  }}
/>

Worth knowing before trusting a figure:

  • Alleles must be written as the listing's minimal block writes them. A deletion is - there and in Ensembl alike, while its VCF form carries an anchor base (CA>C) that no frequency row will ever match. Matching on vcf.alt silently misses every indel; variantAllele picks the right one.
  • Work out the minor-allele frequency rather than reading it. Ensembl reports MAF and minor_allele as null for sorghum even where its own population rows carry real frequencies, so minorAlleleFrequency computes it.
  • Rows repeat. The same population and allele arrive several times over; dedupePopulations collapses them.
  • Panels barely overlap. EVA variants are called in the association panels, EMS ones only in the mutant panels, so with no panel chosen each row falls back to the widest panel that has anything to say about it and names which.
  • A count matters as much as a frequency. 0.56% of 180 lines is one line; the column shows n beside every figure for that reason.
  • About one variant in ten has no frequency reported at all, and one entered by hand has no id to look up.

Restriction sites on a product

Every designed pair carries a restriction map of its predicted product, under the amplicon in the pair detail. This needs no callback at all — template.seq comes back with every design — so it works in every mode, pasted sequence included.

The table lists each enzyme with its recognition sequence, how many sites it has in the product, where those sites are in template coordinates, and the fragment sizes a digest would give; enzymes that cut fewest times come first, because a single cutter splitting the product into two identifiable bands is the useful one. Tick any number of enzymes — each gets its own colour, and their recognition sites are underlined in that colour in the amplicon sequence with the cut points marked. Select all and Select none act on whatever the table is listing. Beneath, the enzymes with no site at all are named — the check before adding a site to a primer end for cloning.

The same selection drives the template map, which carries its own checklist of every enzyme with a site in the template, so sites can be seen across the whole template rather than one amplicon at a time. Ticking an enzyme in either place marks it in both. The map behaves like a genome browser track:

  • Counts follow the view. Zoom or pan and the count beside each enzyme is the number of its sites in view; an enzyme with none there is greyed but stays listed and tickable, since it may have sites a pan away.
  • Colliding marks are bumped into lanes. Packing is done in pixels, so two sites that overlap when zoomed out separate when zoomed in, and the track grows or shrinks to fit — up to eight lanes, past which the last lane takes the overflow.
  • Hover a mark for the enzyme, its location in the template and the genome, the recognition sequence with its cut, and the bases actually there — which differ for a degenerate site such as AccI (GT^MKAC).
  • Colours match the key and are distinct. Each ticked enzyme is allocated a colour from the Okabe-Ito palette that no other ticked enzyme is using, and keeps it while it stays ticked. Up to eight are always told apart; beyond that colours repeat and the names carry the difference. An unticked enzyme has no colour, because it is drawn nowhere.
import { digestAmplicon, singleCutters, nonCutters } from 'gramene-primers';

const span = { start: pair.left.start, end: pair.right.end };
digestAmplicon(template.seq, span); // [{ enzyme, sites, cuts, fragments }, …]
singleCutters(digestAmplicon(template.seq, span)); // the readable diagnostics
nonCutters(template.seq, span); // safe to add to a primer end

sites are {start, end, strand} in template coordinates and cuts are the template positions where the top strand is severed, so both can be drawn directly. Sizes are from sequence alone: methylation, star activity and partial digests are not modelled.

Development

npm install
npm run typecheck
npm test                  # vitest + jsdom unit and component tests
npm run build             # dist/gramene-primers.{js,cjs,css}, dist/index.d.{ts,cts}
npm run lint:pkg          # publint + @arethetypeswrong/cli on a tarball packed in a temp dir
npm run pack:local        # build, then gramene-primers-0.2.0.tgz
npm run fixtures          # contract request fixtures (below)
npm run dev               # playground on :5174

Playground

npm run dev serves examples/playground on http://localhost:5174 (strict port), with gramene-primers resolved to src/. From a workstation: ssh -L 5174:localhost:5174 squam.cshl.edu.

URL parameter Values
api mock (default) replays real design fixtures and simulates check jobs (queued → running with partial results → done); live uses the real client against PRIMERS_API when the dev server has it (e.g. PRIMERS_API=https://data.sorghumbase.org/sorghum_v11a npm run dev, called directly), else against /sorghum_v11, proxied to PRIMERS_PROXY_TARGET (default http://localhost:50111)
page gene-000200 (default), gene-000700, transcript-87700, transcript-46200, region, sequence, check-p1-p3, check-p5l, check-qpcr
mockError Mock only: the first matching request fails with this code. BUSY, VALIDATION, FEATURE_DISABLED, PRIMER3_UNAVAILABLE apply to design; QUEUE_FULL, JOB_TOO_LARGE and any other code (as a 503) apply to the check

Example: http://localhost:5174/?api=mock&page=check-p1-p3&mockError=QUEUE_FULL. The toolbar switches page and theme, toggles controlled state, resets the page, and restores the page with an expired job. The inspector shows the emitted state and the event log.

Fixtures

  • npm run fixtures (test/fixtures.gen.test.ts) writes builder output for every mode to test/fixtures/contract/requests/:
    • design-*.json files are POST /primers/design bodies; check-*.json files are POST /primers/check bodies.
    • test/fixtures/contract/manifest.json lists each file with its swagger definition.
    • Copy the requests into gramene-swagger test/primers/fixtures/contract/requests/ (remove stale files there first). contract.test.js validates them with sway; PRIMERS_CONTRACT_FIXTURES=<dir> points that test at another directory.
  • node scripts/capture-fixtures.mjs stores real API responses in test/fixtures/api/ for component tests and the playground.
    • It needs PRIMERS_IT_BASE=http://localhost:50111/sorghum_v11.
    • Add --checks to include finished check jobs (BLAST on the server), or --only name[,name] for some captures.
    • A check submit that is not 202 prints a warning and makes the script exit non-zero. This happens when the deterministic job already exists, for example after npm run test:it. Delete the job from the dev store or pass --allow-existing.
    • A stale <name>-running.json is removed before polling.
    • Each file is {captured_at, base, method, path, request, status, headers, body}. It refuses to run against production.
  • node test/components/fixtures/build-designs.mjs regenerates test/components/fixtures/designs/*.json (real Primer3 designs verified against the genome). It needs fastaIdx (PRIMERS_FASTAIDX, default http://localhost:8888) and primer3_core (PRIMER3_CORE), so it runs on squam only.
  • test/fixtures/genes/ and test/fixtures/sequences/ hold real SorghumBase v11 gene docs and sequence. Every coordinate in test/fixtures/samples.ts was verified against the genome.

Integration tests (opt-in)

PRIMERS_IT_BASE=http://localhost:50111/sorghum_v11 npm run test:it          # live design/genomes/errors
PRIMERS_IT_BASE=… PRIMERS_IT_CHECKS=1 npm run test:it                      # plus one BLAST check job
PRIMERS_FASTAIDX=http://localhost:8888 npm run test:it                     # coordinates vs genome sequence

License

Apache-2.0 (see LICENSE).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages