, so that
* hubspotform.js (which only loads later, inside the body, on pages that
* actually have a HubSpot Form component) can read the same values
* without duplicating them. hubspotform.js reads this lazily (at init
* time, not at parse time) so it never depends on this script having run
* first -- treat that as defense-in-depth, not a reason to relax this
* script's own placement in
.
*/
window.HS_CONFIG = window.HS_CONFIG || Object.freeze({
PORTAL_ID: '24131897',
GPC_FORM_GUID: '2b423b38-65e9-4811-8fdc-85e52e7d29d8',
DEFAULT_REGION: 'na1'
});
const PORTAL_ID = window.HS_CONFIG.PORTAL_ID;
const GPC_FORM_GUID = window.HS_CONFIG.GPC_FORM_GUID;
// ---------- Generic cookie helpers ----------
function getCookie(name) {
const match = document.cookie.match(
new RegExp('(^|;\\s*)' + name.replace(/[.*+?^{}()|[\]\\$]/g, '\\$&') + '=([^;]*)')
);
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name, value, days) {
const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/; SameSite=Lax; Secure;';
}
// ---------- OptanonConsent parsing ----------
//
// OptanonConsent is a URL-encoded, semi-structured query string, e.g.:
// isGpcEnabled=1&datestamp=2026-09-11T14:32:10Z&version=202401.1.0&
// groups=C0001%3A1%2CC0002%3A1%2CC0003%3A1%2CC0004%3A0&...
//
// Note: OneTrust's cookie format has changed across versions -- verify
// field names against your account's actual cookie before relying on
// this in prod.
function parseOptanonConsent() {
const raw = getCookie('OptanonConsent');
if (!raw) return null;
const params = new URLSearchParams(raw);
const groupsRaw = params.get('groups') || '';
const groups = {};
groupsRaw.split(',').forEach((pair) => {
const parts = pair.split(':');
const id = parts[0];
const status = parts[1];
if (id) groups[id] = status === '1';
});
return {
isGpcEnabled: params.get('isGpcEnabled') === '1',
datestamp: params.get('datestamp'),
groups: groups
};
}
// ---------- Status tracking cookie (keyed by email) ----------
//
// Keyed by email rather than a single flat record. On a shared device, a
// flat "last status sent" record gets overwritten by whoever used the
// browser most recently, causing one person's status to look "already
// sent" for someone else. Keying by email keeps each person's dedupe
// state separate even on a shared browser/cookie jar.
const STATUS_COOKIE_NAME = 'hs_gpc_status_records';
const STATUS_COOKIE_DAYS = 365;
function normalizeEmail(email) {
return String(email || '').trim().toLowerCase();
}
function getAllStatusRecords() {
const raw = getCookie(STATUS_COOKIE_NAME);
if (!raw) return {};
try {
return JSON.parse(raw) || {};
} catch (e) {
return {};
}
}
function getStatusRecordForEmail(email) {
const key = normalizeEmail(email);
if (!key) return null;
return getAllStatusRecords()[key] || null;
}
function setStatusRecordForEmail(email, lastSentGpcStatus, submissionStatus) {
const key = normalizeEmail(email);
if (!key) return;
const records = getAllStatusRecords();
records[key] = {
lastSentGpcStatus: lastSentGpcStatus,
lastUpdated: new Date().toISOString(),
submissionStatus: submissionStatus // "submitted" | "failed" | "pending"
};
// Cookies have a ~4KB practical limit -- prune oldest entries beyond a
// reasonable count so this doesn't grow unbounded on shared devices.
const MAX_RECORDS = 20;
const keys = Object.keys(records);
if (keys.length > MAX_RECORDS) {
const oldestFirst = keys.sort(
(a, b) => new Date(records[a].lastUpdated) - new Date(records[b].lastUpdated)
);
oldestFirst.slice(0, keys.length - MAX_RECORDS).forEach((k) => delete records[k]);
}
setCookie(STATUS_COOKIE_NAME, JSON.stringify(records), STATUS_COOKIE_DAYS);
}
// ---------- Email-identified submission path (preferred) ----------
async function maybeSubmitGpcStatusForEmail(email) {
const key = normalizeEmail(email);
if (!key) return;
const consent = parseOptanonConsent();
if (!consent) return; // OneTrust hasn't initialized yet
const currentGpcStatus = consent.isGpcEnabled === true;
const existing = getStatusRecordForEmail(key);
if (
existing &&
existing.lastSentGpcStatus === currentGpcStatus &&
existing.submissionStatus === 'submitted'
) {
return; // Already recorded this exact status for this specific person
}
setStatusRecordForEmail(key, currentGpcStatus, 'pending');
try {
await submitGpcStatusUpdate({ email: key, gpcStatus: currentGpcStatus });
setStatusRecordForEmail(key, currentGpcStatus, 'submitted');
} catch (err) {
setStatusRecordForEmail(key, currentGpcStatus, 'failed');
}
}
// ---------- hubspotutk-only background path (fallback, higher risk) ----------
//
// Browser-identified, not person-identified -- can misattribute status on
// a genuinely shared device. Kept as a fallback for visitors who never
// submit a form.
const HUBSPOTUTK_RETRY_ATTEMPTS = 5;
const HUBSPOTUTK_RETRY_DELAY_MS = 1000;
async function maybeSubmitGpcStatusBackground(retriesLeft = HUBSPOTUTK_RETRY_ATTEMPTS) {
const hubspotutk = getCookie('hubspotutk');
if (!hubspotutk) {
// hubspotutk is written by HubSpot's own tracking script, which only runs
// (and sets this cookie) after it processes the _hsq doNotTrack push from
// syncHubSpotDoNotTrack — it may not exist yet the instant consent
// resolves. Retry for a bounded window rather than giving up on one miss.
if (retriesLeft > 0) {
setTimeout(() => maybeSubmitGpcStatusBackground(retriesLeft - 1), HUBSPOTUTK_RETRY_DELAY_MS);
}
return;
}
const consent = parseOptanonConsent();
if (!consent) return;
const currentGpcStatus = consent.isGpcEnabled === true;
const dedupeKey = 'hutk:' + hubspotutk;
const existing = getStatusRecordForEmail(dedupeKey);
if (
existing &&
existing.lastSentGpcStatus === currentGpcStatus &&
existing.submissionStatus === 'submitted'
) {
return;
}
setStatusRecordForEmail(dedupeKey, currentGpcStatus, 'pending');
try {
await submitGpcStatusUpdate({ hubspotutk: hubspotutk, gpcStatus: currentGpcStatus });
setStatusRecordForEmail(dedupeKey, currentGpcStatus, 'submitted');
} catch (err) {
setStatusRecordForEmail(dedupeKey, currentGpcStatus, 'failed');
}
}
async function submitGpcStatusUpdate(opts) {
const email = opts.email;
const hubspotutk = opts.hubspotutk;
const gpcStatus = opts.gpcStatus;
const fields = [
{ name: 'consent_optout_gpc', value: String(gpcStatus) },
{ name: 'consent_last_updated_date_gpc', value: new Date().toISOString() }
];
if (email) fields.push({ name: 'email', value: email });
const response = await fetch(
'https://api.hsforms.com/submissions/v3/integration/submit/' + PORTAL_ID + '/' + GPC_FORM_GUID,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fields: fields,
context: hubspotutk ? { hutk: hubspotutk } : {}
})
}
);
if (!response.ok) throw new Error('HubSpot form submission failed: ' + response.status);
return response.json();
}
// Kept as a defensive fallback in case this account's OneTrust setup does
// dispatch it in some configurations — doesn't hurt to also listen.
function onOneTrustGroupsUpdated() {
maybeSubmitGpcStatusBackground();
}
window.addEventListener('OneTrustGroupsUpdated', onOneTrustGroupsUpdated);
if (window.OneTrust) {
// Fast path for when consent is already known (e.g. on a refresh, or a
// returning visitor) — avoids waiting on an event that may not fire again
// if consent isn't changing this load.
maybeSubmitGpcStatusBackground();
}
// ---------- HubSpot form submission hooks (email-identified, preferred) ----------
// Iframe embed (postMessage-based) -- also covers hbspt.forms.create(),
// since that renders the same underlying iframe and fires the same
// postMessage events.
window.addEventListener('message', (event) => {
if (!event.data || event.data.type !== 'hsFormCallback') return;
if (event.data.eventName !== 'onFormSubmitted') return;
const email = event.data.data && event.data.data.submissionValues
? event.data.data.submissionValues.email
: null;
if (email) {
maybeSubmitGpcStatusForEmail(email);
} else {
maybeSubmitGpcStatusBackground();
}
});
// HubSpot's non-iframe ("raw HTML") / v4 forms dispatch this CustomEvent
// on window. event.detail only carries { formId, instanceId } -- actual
// field values must be fetched via the async getFormFieldValues() call.
window.addEventListener('hs-form-event:on-submission:success', (event) => {
if (!window.HubSpotFormsV4) return;
HubSpotFormsV4.getFormFromEvent(event)
.getFormFieldValues()
.then((fieldValues) => {
// "0-1" is HubSpot's fixed objectTypeId for Contact -- forms with
// fields on multiple objects (e.g. Contact + Ticket) can have more
// than one "email" field, so target the Contact's explicitly.
const emailField = fieldValues.find((field) => field.name === '0-1/email');
const email = emailField ? emailField.value : null;
if (email) {
maybeSubmitGpcStatusForEmail(email);
} else {
maybeSubmitGpcStatusBackground();
}
});
});
})();
Practicing in your approved format helps you get comfortable with the tools before it counts.
Screen Reader Compatible
For examinees who are blind or use screen readers such as JAWS or NVDA. These forms also work best with other assistive technology, including voice recognition and keyboard navigation. Confirm software compatibility with your test coordinator or ACT.
Pre-recorded ASL video is available as an approved accommodation for some TestNav tests. If embedded ASL isn’t available, local ASL interpretation may be allowed with approval.
You’ll be taken to a login screen. Leave the fields blank and select “Practice Tests� at the bottom. Then expand a subject and choose the practice format you want.
ACT State and District
For this assessment, there are timed and untimed versions with:
Text to Speech (embedded)
Video American Sign Language
Screen Reader (for use with your JAWS, NVDA, VoiceOver, or alternative navigation)
Request free alternate paper formats of the ACT practice test. Materials may be checked out by students or used to simulate a practice session in school.
A read-aloud support for sighted examinees that accompanies the paper test booklet and answer document. It uses the same words as the human reader script. Testing online instead? Use the Text to Speech practice tests above.
Video ASL for Paper Testing
A practice version of the video ASL support for paper testing, for examinees who need reading support and primarily use ASL in school. It supplements, but does not replace, item content. Order the paper booklet to use alongside it.
Printing Braille Files
What You’ll Need A braille embosser and paper, embossing software (e.g., BrailleBlaster or Duxbury), Adobe Acrobat or Tiger Design Suite. Need help embossing? Contact your school vision services team, state vision resource center, or  for embossing services.
Download the Test Materials
Download the digital Braille Practice Test Folder
Open Downloads to find the folder
Right‑click the folder and select Extract, saving the extracted folder to the default location
In the extracted folder, open a test section folder: English, Mathematics, Reading, Science, or Writing
Locate the BRF files in the folder
Mathematics and Science have two BRF files to choose from (UEB or Nemeth)
BRF files may be embossed or can be accessed using a braille display
Emboss BRF Files
Open the embossing software
From the File menu, select Open or press Ctrl + O, and select the desired BRF file
After the file opens, press Ctrl + E to access the embossing menu
Select Emboss
Emboss Tactile Graphics
(Math & Science only)
In the Mathematics or Science folder, find and open the “Tactile Graphics� folder to find PDF tactile graphic files for each graphic in the subject test
Open PDFs in Adobe Acrobat, or Tiger Design Suite
Ensure your computer is connected to the embosser
Open the Print menu, select the embosser as the printer, and select Print
This action will open a new window. Do you want to proceed?
Welcome to ACT
If you are accessing this site from outside the United States, Puerto Rico, or U.S. Territories, please proceed to the non-U.S. version of our website.