diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 1d4b8b43..3baa9f7c 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -224,6 +224,18 @@ jobs: KONDUIT_APP_NAME: ${{ secrets.KONDUIT_APP_NAME }} steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve review DB config + id: review_db_config + shell: bash + run: | + set -euo pipefail + config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); normalized = case value when true then true when false then false else value.to_s.strip.downcase == 'true' end; puts(normalized ? 'true' : 'false')")" + echo "config_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" + echo "effective_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" + - name: Deploy App to Review id: deploy_review uses: DFE-Digital/github-actions/deploy-to-aks@master @@ -245,18 +257,18 @@ jobs: # REVIEW APP DATABASE DEPLOYMENT (PR) # --------------------------- - name: Checkout (needed for workspace consistency) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: actions/checkout@v4 - name: Install Azure CLI - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - name: Azure login (OIDC) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -264,7 +276,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Install kubectl (pinned) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -274,7 +286,7 @@ jobs: kubectl version --client=true - name: Install kubelogin (pinned) - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -285,7 +297,7 @@ jobs: kubelogin --version - name: Configure AKS credentials - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -295,7 +307,7 @@ jobs: kubelogin convert-kubeconfig -l azurecli - name: Download konduit.sh - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -305,7 +317,7 @@ jobs: ls -la "$GITHUB_WORKSPACE/konduit.sh" - name: Download seed backup from Blob - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: pwsh env: AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} @@ -367,7 +379,7 @@ jobs: } -Label "download $env:BACKUP_BLOB_TEST" } - name: Reset PR review DB - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} @@ -413,7 +425,7 @@ jobs: "${APP_NAME}" -- psql - name: Restore backup into PR review DB - if: ${{ github.event_name != 'workflow_dispatch' || (inputs.environment == 'review' && inputs.refresh-review-db) }} + if: ${{ steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} @@ -492,6 +504,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Resolve review DB config + id: review_db_config + if: ${{ inputs.environment == 'review' }} + shell: bash + run: | + set -euo pipefail + config_reset_review_db="$(ruby -e "require 'yaml'; config = YAML.load_file('terraform/application/config/review.yml') || {}; value = config.fetch('reset_review_db', true); normalized = case value when true then true when false then false else value.to_s.strip.downcase == 'true' end; puts(normalized ? 'true' : 'false')")" + if [ "${config_reset_review_db}" = "true" ] && [ "${{ inputs.refresh-review-db }}" = "true" ]; then + effective_reset_review_db="true" + else + effective_reset_review_db="false" + fi + echo "config_reset_review_db=${config_reset_review_db}" >> "$GITHUB_OUTPUT" + echo "effective_reset_review_db=${effective_reset_review_db}" >> "$GITHUB_OUTPUT" + - name: Deploy app to ${{ matrix.environment }} id: deploy_app uses: DFE-Digital/github-actions/deploy-to-aks@master @@ -646,18 +673,18 @@ jobs: kubectl -n "${namespace}" rollout status deployment/get-school-improvement-insights-maintenance --timeout=180s - name: Checkout (needed for workspace consistency) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: actions/checkout@v4 - name: Install Azure CLI - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - name: Azure login (OIDC) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -665,7 +692,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Install kubectl (pinned) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -675,7 +702,7 @@ jobs: kubectl version --client=true - name: Install kubelogin (pinned) - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -686,7 +713,7 @@ jobs: kubelogin --version - name: Configure AKS credentials - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -696,7 +723,7 @@ jobs: kubelogin convert-kubeconfig -l azurecli - name: Download konduit.sh - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash run: | set -euo pipefail @@ -706,7 +733,7 @@ jobs: ls -la "$GITHUB_WORKSPACE/konduit.sh" - name: Download seed backup from Blob - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} @@ -726,7 +753,7 @@ jobs: ls -lh "${BACKUP_FILE}" - name: Reset PR review DB - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} @@ -772,7 +799,7 @@ jobs: "${APP_NAME}" -- psql - name: Restore backup into PR review DB - if: ${{ inputs.environment == 'review' && inputs.refresh-review-db }} + if: ${{ inputs.environment == 'review' && steps.review_db_config.outputs.effective_reset_review_db == 'true' }} shell: bash env: AKS_NAMESPACE: ${{ secrets.AKS_REVIEW_NAMESPACE }} diff --git a/SAPSec.Web/AssetSrc/js/chart-factory.js b/SAPSec.Web/AssetSrc/js/chart-factory.js index b4eea571..fc47ac02 100644 --- a/SAPSec.Web/AssetSrc/js/chart-factory.js +++ b/SAPSec.Web/AssetSrc/js/chart-factory.js @@ -259,7 +259,207 @@ }; } - function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign) { + function getNumericSeriesValues(chartData) { + if (!chartData || !Array.isArray(chartData.datasets)) { + return []; + } + + return chartData.datasets + .flatMap(function (dataset) { + return Array.isArray(dataset.data) ? dataset.data : []; + }) + .filter(function (value) { + return value !== null && value !== undefined && !Number.isNaN(Number(value)); + }) + .map(Number); + } + + function getNiceStepSize(range) { + if (!range || range <= 0) { + return 1; + } + + const roughStep = range / 4; + const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); + const normalised = roughStep / magnitude; + + if (normalised <= 1) { + return magnitude; + } + + if (normalised <= 2) { + return 2 * magnitude; + } + + if (normalised <= 5) { + return 5 * magnitude; + } + + return 10 * magnitude; + } + + function roundDownToStep(value, step) { + return Math.floor(value / step) * step; + } + + function roundUpToStep(value, step) { + return Math.ceil(value / step) * step; + } + + function getDynamicLineAxisConfig(chartData, axisSuffix) { + const values = getNumericSeriesValues(chartData); + if (!values.length) { + return null; + } + + const rawMin = Math.min.apply(null, values); + const rawMax = Math.max.apply(null, values); + const range = rawMax - rawMin; + const padding = range === 0 + ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === '%' ? 2 : 1) + : Math.max(range * 0.2, axisSuffix === '%' ? 2 : 1); + + let min = rawMin - padding; + let max = rawMax + padding; + + if (axisSuffix === '%') { + min = Math.max(0, min); + max = Math.min(100, max); + } + + if (min === max) { + max = min + (axisSuffix === '%' ? 4 : 2); + } + + const step = getNiceStepSize(max - min); + + return { + min: roundDownToStep(min, step), + max: roundUpToStep(max, step), + step + }; + } + + function formatTooltipValue(value, axisSuffix, decimals) { + if (value === null || value === undefined || Number.isNaN(Number(value))) { + return 'No data'; + } + + const numericValue = Number(value); + const formattedValue = decimals !== null && decimals !== undefined + ? numericValue.toFixed(decimals) + : numericValue; + + return `${formattedValue}${axisSuffix}`; + } + + function getTooltipContainer(chart) { + return chart.canvas.closest('.app-ks4-chart-container') || chart.canvas.parentElement; + } + + function getOrCreateHtmlTooltip(chart) { + const container = getTooltipContainer(chart); + if (!container) { + return null; + } + + let tooltip = container.querySelector(`.app-chart-tooltip[data-chart-id="${chart.canvas.id}"]`); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'app-chart-tooltip'; + tooltip.setAttribute('data-chart-id', chart.canvas.id); + + const title = document.createElement('div'); + title.className = 'app-chart-tooltip__title'; + tooltip.appendChild(title); + + const body = document.createElement('div'); + body.className = 'app-chart-tooltip__body'; + tooltip.appendChild(body); + + container.appendChild(tooltip); + } + + return tooltip; + } + + function hideAllHtmlTooltips() { + document.querySelectorAll('.app-chart-tooltip--visible').forEach(function (tooltip) { + tooltip.classList.remove('app-chart-tooltip--visible'); + }); + } + + function renderHtmlTooltip(context, axisSuffix, tooltipDecimals) { + const { chart, tooltip } = context; + const tooltipElement = getOrCreateHtmlTooltip(chart); + + if (!tooltipElement) { + return; + } + + if (!tooltip || tooltip.opacity === 0) { + tooltipElement.classList.remove('app-chart-tooltip--visible'); + return; + } + + const titleElement = tooltipElement.querySelector('.app-chart-tooltip__title'); + const bodyElement = tooltipElement.querySelector('.app-chart-tooltip__body'); + + if (!titleElement || !bodyElement) { + return; + } + + titleElement.textContent = tooltip.title?.[0] ?? ''; + bodyElement.innerHTML = ''; + + tooltip.dataPoints.forEach(function (point) { + const row = document.createElement('div'); + row.className = 'app-chart-tooltip__row'; + + const marker = document.createElement('span'); + marker.className = 'app-chart-tooltip__marker'; + marker.style.backgroundColor = point.dataset.borderColor || point.dataset.backgroundColor || CHART_CONFIG.fallbacks.legendBoxColor; + + const label = document.createElement('span'); + label.className = 'app-chart-tooltip__label'; + label.textContent = point.dataset.label || ''; + + const value = document.createElement('span'); + value.className = 'app-chart-tooltip__value'; + value.textContent = formatTooltipValue(point.parsed.y, axisSuffix, tooltipDecimals); + + row.appendChild(marker); + row.appendChild(label); + row.appendChild(value); + bodyElement.appendChild(row); + }); + + tooltipElement.classList.add('app-chart-tooltip--visible'); + + const container = getTooltipContainer(chart); + if (!container) { + return; + } + + const canvasRect = chart.canvas.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + const tooltipWidth = tooltipElement.offsetWidth; + const gap = 16; + const pointLeft = canvasRect.left - containerRect.left + tooltip.caretX; + const rightCandidate = pointLeft + gap; + const leftCandidate = pointLeft - tooltipWidth - gap; + const containerWidth = container.clientWidth; + const hasRoomOnRight = rightCandidate + tooltipWidth <= containerWidth - gap; + const left = hasRoomOnRight + ? rightCandidate + : Math.max(gap, leftCandidate); + const top = canvasRect.top - containerRect.top + tooltip.caretY; + + tooltipElement.style.left = `${left}px`; + tooltipElement.style.top = `${top}px`; + } + + function buildChartOptions(type, gdsStyles, axisStep, axisSuffix, axisMin, axisMax, axisAutoSkip, showLegend, showDataLabels, showXGrid, barLabelAlign, dynamicLineAxis, tooltipDecimals) { const common = { responsive: true, maintainAspectRatio: false, @@ -271,15 +471,18 @@ size: gdsStyles.fontSize }; - const stepSize = axisStep; - const axisTickCount = axisMin !== null && axisMax !== null && stepSize - ? Math.floor((axisMax - axisMin) / stepSize) + 1 + const resolvedAxisMin = dynamicLineAxis ? dynamicLineAxis.min : axisMin; + const resolvedAxisMax = dynamicLineAxis ? dynamicLineAxis.max : axisMax; + const stepSize = dynamicLineAxis ? dynamicLineAxis.step : axisStep; + const axisTickCount = resolvedAxisMin !== null && resolvedAxisMax !== null && stepSize + ? Math.floor((resolvedAxisMax - resolvedAxisMin) / stepSize) + 1 : undefined; - const explicitTicks = buildExplicitTicks(axisMin, axisMax, stepSize); + const explicitTicks = buildExplicitTicks(resolvedAxisMin, resolvedAxisMax, stepSize); const legendOptions = { - display: showLegend, + display: type === 'line' ? false : showLegend, position: CHART_CONFIG.legend.position, + align: 'center', labels: { usePointStyle: true, pointStyle: CHART_CONFIG.legend.pointStyle, @@ -292,6 +495,10 @@ if (type === 'line') { return { ...common, + interaction: { + mode: 'index', + intersect: false + }, layout: { padding: { top: CHART_CONFIG.line.layout.topPadding, @@ -300,9 +507,9 @@ }, scales: { y: { - beginAtZero: true, - min: axisMin ?? undefined, - max: axisMax ?? undefined, + beginAtZero: !dynamicLineAxis, + min: resolvedAxisMin ?? undefined, + max: resolvedAxisMax ?? undefined, grace: CHART_CONFIG.line.axis.grace, afterBuildTicks: explicitTicks, grid: { @@ -340,7 +547,22 @@ } }, plugins: { - tooltip: { enabled: false }, + tooltip: { + enabled: false, + external: function (context) { + renderHtmlTooltip(context, axisSuffix, tooltipDecimals); + }, + callbacks: { + title: function (contexts) { + return contexts?.[0]?.label ?? ''; + }, + label: function (context) { + const label = context.dataset.label ? context.dataset.label + ': ' : ''; + const value = context.parsed.y; + return `${label}${formatTooltipValue(value, axisSuffix, tooltipDecimals)}`; + } + } + }, legend: legendOptions, title: { display: false, @@ -452,6 +674,9 @@ return common; } + window.addEventListener('scroll', hideAllHtmlTooltips, { passive: true }); + window.addEventListener('resize', hideAllHtmlTooltips, { passive: true }); + const noDataBarLabelsPlugin = { id: 'noDataBarLabels', afterDraw(chart, args, pluginOptions) { @@ -573,6 +798,13 @@ return ks4CoreSubjectYearByYearChartIds.has(canvas.id); } + function isYearByYearLineChart(canvas) { + return canvas && ( + canvas.id.includes('yearbyyear-chart') + || canvas.id.includes('year-by-year-chart') + ); + } + function initCharts() { document.querySelectorAll('.js-chart').forEach(canvas => { if (charts[canvas.id]) { @@ -587,10 +819,11 @@ const chartData = JSON.parse(canvas.dataset.chart); const type = canvas.dataset.type; + const showLegend = canvas.dataset.showLegend === "true"; + if (type === 'bar') { resizeBarChartContainer(canvas, chartData); } - const showLegend = canvas.dataset.showLegend === "true"; const showDataLabels = canvas.dataset.showDatalabels !== "false"; const showXGrid = canvas.dataset.showXGrid === "true"; const forceKs4CoreSubjectTicks = isKs4CoreSubjectYearByYearChart(canvas); @@ -612,6 +845,12 @@ const labelDecimals = canvas.dataset.labelDecimals ? parseInt(canvas.dataset.labelDecimals, 10) : null; + const tooltipDecimals = canvas.dataset.tooltipDecimals + ? parseInt(canvas.dataset.tooltipDecimals, 10) + : null; + const dynamicLineAxis = type === 'line' && isYearByYearLineChart(canvas) + ? getDynamicLineAxisConfig(chartData, axisSuffix) + : null; const rawColors = canvas.dataset.colors ? JSON.parse(canvas.dataset.colors) @@ -652,7 +891,9 @@ showLegend, showDataLabels, showXGrid, - barLabelAlign), + barLabelAlign, + dynamicLineAxis, + tooltipDecimals), plugins: [ ...(showDataLabels ? [ChartDataLabels] : []), noDataBarLabelsPlugin @@ -680,9 +921,9 @@ charts[canvas.id] = chart; if (showLegend) { - const legendContainer = document.querySelector( - `.chart-legend[data-chart-id="${canvas.id}"]` - ); + const legendContainer = type === 'line' + ? ensureTopLegendContainer(canvas) + : document.querySelector(`.chart-legend[data-chart-id="${canvas.id}"]`); if (legendContainer) { buildVerticalLegend(chart, legendContainer); } @@ -709,19 +950,24 @@ } function buildVerticalLegend(chart, container) { + container.innerHTML = ''; + const datasets = Array.isArray(chart.data.datasets) ? chart.data.datasets : [chart.data.datasets]; const ul = document.createElement('ul'); + ul.classList.add('app-chart-legend'); datasets.forEach(ds => { const li = document.createElement('li'); + li.classList.add('app-chart-legend__item'); const box = document.createElement('span'); - box.classList.add('legend-box'); + box.classList.add('app-chart-legend__box'); box.style.backgroundColor = ds.backgroundColor || ds.borderColor || CHART_CONFIG.fallbacks.legendBoxColor; const label = document.createElement('span'); + label.classList.add('app-chart-legend__label'); label.textContent = ds.label; li.appendChild(box); @@ -732,6 +978,28 @@ container.appendChild(ul); } + function ensureTopLegendContainer(canvas) { + const chartContainer = canvas.parentElement; + const legendHost = chartContainer?.parentElement; + if (!chartContainer || !legendHost) { + return null; + } + + let legendContainer = chartContainer.previousElementSibling; + if (legendContainer?.getAttribute('data-chart-id') !== canvas.id || !legendContainer.classList.contains('chart-legend')) { + legendContainer = null; + } + + if (!legendContainer) { + legendContainer = document.createElement('div'); + legendContainer.className = 'chart-legend chart-legend--top'; + legendContainer.setAttribute('data-chart-id', canvas.id); + legendHost.insertBefore(legendContainer, chartContainer); + } + + return legendContainer; + } + function adjustChartResize() { let resizeTimeout; window.addEventListener('resize', () => { @@ -740,7 +1008,7 @@ Object.values(charts).forEach(chart => { const fontSizePx = gdsVars(chart.canvas).fontSize; - if (chart.options.scales.x.ticks.font) { + if (chart.options.scales.x.ticks.font && typeof chart.options.scales.x.ticks.font !== 'function') { chart.options.scales.x.ticks.font.size = fontSizePx; } diff --git a/SAPSec.Web/AssetSrc/js/chart-toggle.js b/SAPSec.Web/AssetSrc/js/chart-toggle.js new file mode 100644 index 00000000..10a0a9e3 --- /dev/null +++ b/SAPSec.Web/AssetSrc/js/chart-toggle.js @@ -0,0 +1,184 @@ +(function () { + function setHidden(element, hidden) { + if (!element) { + return; + } + + if (hidden) { + element.setAttribute("hidden", "hidden"); + } else { + element.removeAttribute("hidden"); + } + } + + function resizeCharts(container) { + if (!window.Chart || !container) { + return; + } + + container.querySelectorAll("canvas").forEach(function (canvas) { + var chart = window.Chart.getChart(canvas); + if (!chart) { + return; + } + + chart.resize(); + chart.update("none"); + }); + } + + function getTabTarget(tabLink) { + if (!tabLink) { + return ""; + } + + return tabLink.getAttribute("href") || ""; + } + + function isAverageTabTarget(target) { + return /^#.+three-year-average$/i.test(target) || target === "#three-year-average"; + } + + function isYearByYearTabTarget(target) { + return /^#.+year-by-year$/i.test(target) || target === "#year-by-year"; + } + + function buildToggleHeader() { + var header = document.createElement("div"); + header.className = "app-content-toggle__header"; + + var title = document.createElement("h3"); + title.className = "govuk-heading-m app-content-toggle__title"; + title.textContent = "3-year average"; + + var button = document.createElement("button"); + button.type = "button"; + button.className = "govuk-button govuk-button--secondary"; + button.textContent = "Show year by year"; + button.setAttribute("aria-pressed", "false"); + button.setAttribute("data-module", "govuk-button"); + + header.appendChild(title); + header.appendChild(button); + + return { header: header, title: title, button: button }; + } + + function moveChartBlock(chartContainer, targetPanel) { + if (!chartContainer || !targetPanel) { + return; + } + + var chartCanvas = chartContainer.querySelector("canvas"); + var expectedChartId = chartCanvas ? chartCanvas.id : ""; + var previousSibling = chartContainer.previousElementSibling; + var legend = previousSibling + && previousSibling.classList.contains("chart-legend") + && previousSibling.getAttribute("data-chart-id") === expectedChartId + ? previousSibling + : null; + + if (legend) { + targetPanel.appendChild(legend); + } + + targetPanel.appendChild(chartContainer); + } + + function initialiseTabSet(tabSet) { + var listItems = tabSet.querySelectorAll(".govuk-tabs__list-item"); + if (listItems.length < 2) { + return; + } + + var firstTab = listItems[0].querySelector(".govuk-tabs__tab"); + var secondTab = listItems[1].querySelector(".govuk-tabs__tab"); + var firstPanel = tabSet.querySelector(".govuk-tabs__panel"); + var secondPanel = listItems[1] && secondTab + ? tabSet.querySelector(secondTab.getAttribute("href")) + : null; + + if (!firstTab || !secondTab || !firstPanel || !secondPanel) { + return; + } + + var firstTabTarget = getTabTarget(firstTab); + var secondTabTarget = getTabTarget(secondTab); + + if (!isAverageTabTarget(firstTabTarget) || !isYearByYearTabTarget(secondTabTarget)) { + return; + } + + var averageChart = firstPanel.querySelector(".app-ks4-chart-container"); + var yearlyChart = secondPanel.querySelector(".app-ks4-chart-container"); + + if (!averageChart || !yearlyChart || firstPanel.querySelector(".app-content-toggle")) { + return; + } + + var chartsPanelId = firstTabTarget.slice(1).replace(/three-year-average$/i, "charts"); + + firstTab.textContent = "Charts"; + firstTab.setAttribute("href", "#" + chartsPanelId); + firstPanel.id = chartsPanelId; + listItems[1].remove(); + + var toggleContainer = document.createElement("div"); + toggleContainer.className = "app-content-toggle"; + toggleContainer.setAttribute("data-module", "app-content-toggle"); + + var toggle = buildToggleHeader(); + toggleContainer.appendChild(toggle.header); + + var averagePanel = document.createElement("div"); + averagePanel.className = "app-content-toggle__panel app-content-toggle__panel--active"; + averagePanel.setAttribute("data-content-toggle-panel", "true"); + averagePanel.setAttribute("data-content-toggle-name", "3-year average"); + averagePanel.id = firstTabTarget.slice(1); + moveChartBlock(averageChart, averagePanel); + + var yearlyPanel = document.createElement("div"); + yearlyPanel.className = "app-content-toggle__panel"; + yearlyPanel.setAttribute("data-content-toggle-panel", "true"); + yearlyPanel.setAttribute("data-content-toggle-name", "Year by year"); + yearlyPanel.id = secondTabTarget.slice(1); + yearlyPanel.setAttribute("hidden", "hidden"); + moveChartBlock(yearlyChart, yearlyPanel); + + toggleContainer.appendChild(averagePanel); + toggleContainer.appendChild(yearlyPanel); + + firstPanel.insertBefore(toggleContainer, firstPanel.firstChild); + + var showingYearly = false; + + toggle.button.addEventListener("click", function () { + showingYearly = !showingYearly; + + averagePanel.classList.toggle("app-content-toggle__panel--active", !showingYearly); + yearlyPanel.classList.toggle("app-content-toggle__panel--active", showingYearly); + + setHidden(averagePanel, showingYearly); + setHidden(yearlyPanel, !showingYearly); + + toggle.title.textContent = showingYearly ? "Year by year" : "3-year average"; + toggle.button.textContent = showingYearly ? "Show 3-year average" : "Show year by year"; + toggle.button.setAttribute("aria-pressed", showingYearly ? "true" : "false"); + + resizeCharts(showingYearly ? yearlyPanel : averagePanel); + }); + + secondPanel.remove(); + } + + function init() { + document.querySelectorAll(".app-ks4-tabs").forEach(initialiseTabSet); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + return; + } + + init(); +})(); diff --git a/SAPSec.Web/AssetSrc/js/content-toggle.js b/SAPSec.Web/AssetSrc/js/content-toggle.js new file mode 100644 index 00000000..86336154 --- /dev/null +++ b/SAPSec.Web/AssetSrc/js/content-toggle.js @@ -0,0 +1,84 @@ +(function () { + function setHidden(element, hidden) { + if (!element) { + return; + } + + if (hidden) { + element.setAttribute("hidden", "hidden"); + } else { + element.removeAttribute("hidden"); + } + } + + function resizeCharts(container) { + if (!window.Chart || !container) { + return; + } + + container.querySelectorAll("canvas").forEach(function (canvas) { + var chart = window.Chart.getChart(canvas); + if (!chart) { + return; + } + + chart.resize(); + chart.update("none"); + }); + } + + function initialiseToggle(toggle) { + var title = toggle.querySelector(".app-content-toggle__title"); + var button = toggle.querySelector(".app-content-toggle__header button[type='button']"); + var panels = Array.prototype.slice.call(toggle.querySelectorAll("[data-content-toggle-panel]")); + + if (!title || !button || panels.length < 2) { + return; + } + + var activeIndex = panels.findIndex(function (panel) { + return !panel.hasAttribute("hidden"); + }); + + if (activeIndex < 0) { + activeIndex = 0; + } + + function render(index) { + var nextIndex = (index + 1) % panels.length; + var activePanel = panels[index]; + var nextPanel = panels[nextIndex]; + var activeName = activePanel.getAttribute("data-content-toggle-name") || ""; + var nextName = nextPanel.getAttribute("data-content-toggle-name") || ""; + + panels.forEach(function (panel, panelIndex) { + panel.classList.toggle("app-content-toggle__panel--active", panelIndex === index); + setHidden(panel, panelIndex !== index); + }); + + title.textContent = activeName; + button.textContent = "Show " + nextName.toLowerCase(); + button.setAttribute("aria-pressed", index === 0 ? "false" : "true"); + + resizeCharts(activePanel); + } + + button.addEventListener("click", function () { + activeIndex = (activeIndex + 1) % panels.length; + render(activeIndex); + }); + + render(activeIndex); + } + + function init() { + document.querySelectorAll(".app-content-toggle").forEach(initialiseToggle); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + return; + } + + init(); +})(); diff --git a/SAPSec.Web/AssetSrc/js/data-view-switcher.js b/SAPSec.Web/AssetSrc/js/data-view-switcher.js index 06ea7e08..d7350a17 100644 --- a/SAPSec.Web/AssetSrc/js/data-view-switcher.js +++ b/SAPSec.Web/AssetSrc/js/data-view-switcher.js @@ -1,4 +1,136 @@ (function () { + function getNumericSeriesValues(seriesMap, seriesKeys) { + return seriesKeys + .flatMap(function (seriesKey) { + var values = seriesMap && Array.isArray(seriesMap[seriesKey]) ? seriesMap[seriesKey] : []; + return values; + }) + .filter(function (value) { + return value !== null && value !== undefined && !Number.isNaN(Number(value)); + }) + .map(Number); + } + + function getNiceStepSize(range) { + if (!range || range <= 0) { + return 1; + } + + var roughStep = range / 4; + var magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); + var normalised = roughStep / magnitude; + + if (normalised <= 1) { + return magnitude; + } + + if (normalised <= 2) { + return 2 * magnitude; + } + + if (normalised <= 5) { + return 5 * magnitude; + } + + return 10 * magnitude; + } + + function roundDownToStep(value, step) { + return Math.floor(value / step) * step; + } + + function roundUpToStep(value, step) { + return Math.ceil(value / step) * step; + } + + function buildExplicitTicks(axisMin, axisMax, stepSize) { + if (axisMin === null || axisMax === null || !stepSize) { + return undefined; + } + + return function (axis) { + var ticks = []; + for (var value = axisMin; value <= axisMax; value += stepSize) { + ticks.push({ value: value }); + } + axis.ticks = ticks; + }; + } + + function getDynamicLineAxisConfig(seriesMap, seriesKeys, axisSuffix) { + var values = getNumericSeriesValues(seriesMap, seriesKeys); + if (!values.length) { + return null; + } + + var rawMin = Math.min.apply(null, values); + var rawMax = Math.max.apply(null, values); + var range = rawMax - rawMin; + var padding = range === 0 + ? Math.max(Math.abs(rawMax) * 0.1, axisSuffix === "%" ? 2 : 1) + : Math.max(range * 0.2, axisSuffix === "%" ? 2 : 1); + + var min = rawMin - padding; + var max = rawMax + padding; + + if (axisSuffix === "%") { + min = Math.max(0, min); + max = Math.min(100, max); + } + + if (min === max) { + max = min + (axisSuffix === "%" ? 4 : 2); + } + + var step = getNiceStepSize(max - min); + + return { + min: roundDownToStep(min, step), + max: roundUpToStep(max, step), + step: step + }; + } + + function isYearByYearLineChart(chart) { + var chartId = chart && chart.canvas ? chart.canvas.id : ""; + return chartId.indexOf("yearbyyear-chart") >= 0 || chartId.indexOf("year-by-year-chart") >= 0; + } + + function updateLineChartAxis(lineChart, seriesMap, seriesKeys) { + if (!lineChart || !lineChart.options || !lineChart.options.scales || !lineChart.options.scales.y) { + return; + } + + if (!isYearByYearLineChart(lineChart)) { + return; + } + + var axisSuffix = lineChart.canvas && lineChart.canvas.dataset + ? (lineChart.canvas.dataset.axisSuffix || "%") + : "%"; + var dynamicAxis = getDynamicLineAxisConfig(seriesMap, seriesKeys, axisSuffix); + var yScale = lineChart.options.scales.y; + + if (!dynamicAxis) { + yScale.min = undefined; + yScale.max = undefined; + yScale.afterBuildTicks = undefined; + if (yScale.ticks) { + yScale.ticks.stepSize = undefined; + yScale.ticks.count = undefined; + } + return; + } + + yScale.min = dynamicAxis.min; + yScale.max = dynamicAxis.max; + yScale.afterBuildTicks = buildExplicitTicks(dynamicAxis.min, dynamicAxis.max, dynamicAxis.step); + if (yScale.ticks) { + yScale.ticks.stepSize = dynamicAxis.step; + yScale.ticks.count = Math.floor((dynamicAxis.max - dynamicAxis.min) / dynamicAxis.step) + 1; + } + } + function updateTableRow(cells, values) { cells.forEach(function (cell, index) { if (cell) { @@ -117,6 +249,7 @@ config.seriesKeys.forEach(function (seriesKey, index) { lineChart.data.datasets[index].data = data.line && data.line[seriesKey] ? data.line[seriesKey] : []; }); + updateLineChartAxis(lineChart, data.line, config.seriesKeys); lineChart.update(); } } diff --git a/SAPSec.Web/AssetSrc/scss/main.scss b/SAPSec.Web/AssetSrc/scss/main.scss index 093a3d04..8ef2ee03 100644 --- a/SAPSec.Web/AssetSrc/scss/main.scss +++ b/SAPSec.Web/AssetSrc/scss/main.scss @@ -1201,14 +1201,142 @@ $app-ks4-color-track: #f3f2f1; } .app-ks4-chart-container { + display: flex; + flex-direction: column; height: 260px; max-width: 760px; + position: relative; } .app-ks4-chart-container--school-headline { height: 330px; } +.app-ks4-chart-container .js-chart { + flex: 1 1 auto; + min-height: 0; +} + +.app-content-toggle__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; + padding-top: 10px; +} + +.app-content-toggle__title { + margin-bottom: 0; + @include govuk-font($size: 19, $weight: bold); +} + +.app-content-toggle__button { + @include govuk-font($size: 24); + margin-bottom: 0; +} + +.app-content-toggle .govuk-button { + margin-bottom: 0; +} + +.app-content-toggle__panel[hidden] { + display: none !important; +} + +.chart-legend--top { + flex: 0 0 auto; + margin-bottom: 12px; +} + +.app-chart-legend { + list-style: none; + margin: 0; + padding: 0; + font-size: 14px; + line-height: 1.4; +} + +.app-chart-legend__item { + align-items: center; + display: flex; + gap: 8px; + margin-bottom: 6px; +} + +.app-chart-legend__item:last-child { + margin-bottom: 0; +} + +.app-chart-legend__label { + color: #0b0c0c; +} + +.app-chart-legend__box { + border-radius: 50%; + display: inline-block; + flex: 0 0 14px; + height: 14px; + width: 14px; +} + +.app-chart-tooltip { + background: #ffffff; + border: 1px solid #b1b4b6; + border-radius: 5px; + color: #0b0c0c; + display: none; + left: 0; + max-width: 280px; + padding: 10px 12px; + pointer-events: none; + position: absolute; + top: 0; + transform: translate(0, -50%); + z-index: 20; +} + +.app-chart-tooltip--visible { + display: block; +} + +.app-chart-tooltip__title { + font-size: 14px; + font-weight: 700; + line-height: 1.4; + margin-bottom: 6px; +} + +.app-chart-tooltip__body { + display: flex; + flex-direction: column; + gap: 6px; +} + +.app-chart-tooltip__row { + align-items: center; + display: grid; + gap: 8px; + grid-template-columns: 14px 1fr auto; +} + +.app-chart-tooltip__marker { + border-radius: 50%; + display: block; + height: 14px; + width: 14px; +} + +.app-chart-tooltip__label, +.app-chart-tooltip__value { + font-size: 14px; + line-height: 1.4; +} + +.app-chart-tooltip__value { + font-weight: 700; +} + .app-ks4-copy { max-width: 660px; line-height: 1.45; @@ -1293,6 +1421,18 @@ $app-ks4-color-track: #f3f2f1; -webkit-overflow-scrolling: touch; } + .app-content-toggle__header { + align-items: stretch; + flex-direction: column; + } + + .app-content-toggle__button { + align-self: stretch; + @include govuk-font($size: 19); + text-align: center; + width: 100%; + } + .app-ks4-tabs .govuk-table { min-width: 40rem; } diff --git a/SAPSec.Web/TagHelpers/ContentToggleItem.cs b/SAPSec.Web/TagHelpers/ContentToggleItem.cs new file mode 100644 index 00000000..14375569 --- /dev/null +++ b/SAPSec.Web/TagHelpers/ContentToggleItem.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Html; + +namespace SAPSec.Web.TagHelpers; + +public sealed record ContentToggleItem(string Id, string Name, IHtmlContent Content, bool Active); diff --git a/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs b/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs new file mode 100644 index 00000000..8f46727a --- /dev/null +++ b/SAPSec.Web/TagHelpers/ContentToggleTagHelper.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace SAPSec.Web.TagHelpers; + +[HtmlTargetElement("content-toggle")] +public class ContentToggleTagHelper : TagHelper +{ + [HtmlAttributeName("id")] + public string? Id { get; set; } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var items = new List(); + context.Items[ToggleContentTagHelper.ContextKey] = items; + + await output.GetChildContentAsync(); + + if (items.Count < 2) + { + output.SuppressOutput(); + return; + } + + var activeIndex = items.FindIndex(item => item.Active); + if (activeIndex < 0) + { + activeIndex = 0; + } + + output.TagName = "div"; + output.TagMode = TagMode.StartTagAndEndTag; + output.Attributes.SetAttribute("class", "app-content-toggle"); + output.Attributes.SetAttribute("data-module", "app-content-toggle"); + + if (!string.IsNullOrWhiteSpace(Id)) + { + output.Attributes.SetAttribute("id", Id); + } + + var header = new TagBuilder("div"); + header.AddCssClass("app-content-toggle__header"); + + var title = new TagBuilder("h3"); + title.AddCssClass("govuk-heading-m"); + title.AddCssClass("app-content-toggle__title"); + title.InnerHtml.Append(items[activeIndex].Name); + + var button = new TagBuilder("button"); + button.Attributes["type"] = "button"; + button.AddCssClass("govuk-button"); + button.AddCssClass("govuk-button--secondary"); + button.Attributes["aria-pressed"] = activeIndex == 0 ? "false" : "true"; + button.Attributes["data-module"] = "govuk-button"; + button.InnerHtml.Append($"Show {items[(activeIndex + 1) % items.Count].Name.ToLowerInvariant()}"); + + header.InnerHtml.AppendHtml(title); + header.InnerHtml.AppendHtml(button); + + output.Content.AppendHtml(header); + + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + var panel = new TagBuilder("div"); + panel.AddCssClass("app-content-toggle__panel"); + panel.Attributes["data-content-toggle-panel"] = "true"; + panel.Attributes["data-content-toggle-name"] = item.Name; + + if (!string.IsNullOrWhiteSpace(item.Id)) + { + panel.Attributes["id"] = item.Id; + } + + if (i == activeIndex) + { + panel.AddCssClass("app-content-toggle__panel--active"); + } + else + { + panel.Attributes["hidden"] = "hidden"; + } + + panel.InnerHtml.AppendHtml(item.Content); + output.Content.AppendHtml(panel); + } + } +} diff --git a/SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs b/SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs new file mode 100644 index 00000000..04f4d607 --- /dev/null +++ b/SAPSec.Web/TagHelpers/ToggleContentTagHelper.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace SAPSec.Web.TagHelpers; + +[HtmlTargetElement("toggle-content", ParentTag = "content-toggle")] +public class ToggleContentTagHelper : TagHelper +{ + internal static readonly object ContextKey = new(); + + [HtmlAttributeName("id")] + public string Id { get; set; } = string.Empty; + + [HtmlAttributeName("name")] + public string Name { get; set; } = string.Empty; + + [HtmlAttributeName("active")] + public bool Active { get; set; } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (context.Items.TryGetValue(ContextKey, out var items) + && items is IList toggleItems) + { + var childContent = await output.GetChildContentAsync(); + toggleItems.Add(new ContentToggleItem(Id, Name, new HtmlString(childContent.GetContent()), Active)); + } + + output.SuppressOutput(); + } +} diff --git a/SAPSec.Web/Views/School/Attendance.cshtml b/SAPSec.Web/Views/School/Attendance.cshtml index 70d207fa..8fcf7fbb 100644 --- a/SAPSec.Web/Views/School/Attendance.cshtml +++ b/SAPSec.Web/Views/School/Attendance.cshtml @@ -122,6 +122,7 @@ data-type="line" data-axis-mode="auto" data-axis-suffix="%" + data-tooltip-decimals="2" data-show-x-grid="true" data-show-legend="true" data-show-datalabels="false" @@ -215,5 +216,6 @@ + } diff --git a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml index c4fdecb9..e2e61043 100644 --- a/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/School/Ks4CoreSubjects.cshtml @@ -132,7 +132,7 @@
- +
@@ -235,7 +235,7 @@
- +
@@ -338,7 +338,7 @@
- +
@@ -441,7 +441,7 @@
- +
@@ -544,7 +544,7 @@
- +
@@ -647,7 +647,7 @@
- +
@@ -750,7 +750,7 @@
- +
@@ -825,5 +825,6 @@ + } diff --git a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml index fb51e86f..168a353b 100644 --- a/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/School/Ks4HeadlineMeasures.cshtml @@ -58,56 +58,59 @@
-
-
- @{ - var attainmentBarData = new decimal?[] { Model.SchoolAttainment8ThreeYearAverage, Model.SimilarSchoolsAttainment8ThreeYearAverage, Model.LocalAuthorityAttainment8ThreeYearAverage, Model.EnglandAttainment8ThreeYearAverage }; - } - @if (HasAnyData(attainmentBarData)) - { - - - } - else - { -

No available data

- } -
-
- -
-
- - -
+
+ + +
+ @{ + var attainmentBarData = new decimal?[] { Model.SchoolAttainment8ThreeYearAverage, Model.SimilarSchoolsAttainment8ThreeYearAverage, Model.LocalAuthorityAttainment8ThreeYearAverage, Model.EnglandAttainment8ThreeYearAverage }; + } + @if (HasAnyData(attainmentBarData)) + { + + + } + else + { +

No available data

+ } +
+
+ +
+ + +
+
+
@@ -190,30 +193,33 @@
-
-
- @{ - var engMathsBarData = new decimal?[] { Model.SchoolEngMathsThreeYearAverage, Model.SimilarSchoolsEngMathsThreeYearAverage, Model.LocalAuthorityEngMathsThreeYearAverage, Model.EnglandEngMathsThreeYearAverage }; - } - @if (HasAnyData(engMathsBarData)) - { - - } - else - { -

No available data

- } -
-
-
-
- -
+
+ + +
+ @{ + var engMathsBarData = new decimal?[] { Model.SchoolEngMathsThreeYearAverage, Model.SimilarSchoolsEngMathsThreeYearAverage, Model.LocalAuthorityEngMathsThreeYearAverage, Model.EnglandEngMathsThreeYearAverage }; + } + @if (HasAnyData(engMathsBarData)) + { + + } + else + { +

No available data

+ } +
+
+ +
+ +
+
+

These are the top performing schools for this measure.

@@ -306,30 +312,33 @@
-
-
- @{ - var destinationsBarData = new decimal?[] { Model.SchoolDestinationsThreeYearAverage, Model.SimilarSchoolsDestinationsThreeYearAverage, Model.LocalAuthorityDestinationsThreeYearAverage, Model.EnglandDestinationsThreeYearAverage }; - } - @if (HasAnyData(destinationsBarData)) - { - - } - else - { -

No available data

- } -
-
-
-
- -
+
+ + +
+ @{ + var destinationsBarData = new decimal?[] { Model.SchoolDestinationsThreeYearAverage, Model.SimilarSchoolsDestinationsThreeYearAverage, Model.LocalAuthorityDestinationsThreeYearAverage, Model.EnglandDestinationsThreeYearAverage }; + } + @if (HasAnyData(destinationsBarData)) + { + + } + else + { +

No available data

+ } +
+
+ +
+ +
+
+

These are the top performing schools for this measure.

@@ -390,5 +399,6 @@ + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml index fc709fbd..8c48206a 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Attendance.cshtml @@ -93,6 +93,7 @@ data-type="line" data-axis-mode="auto" data-axis-suffix="%" + data-tooltip-decimals="2" data-show-x-grid="true" data-show-legend="true" data-show-datalabels="false" @@ -148,5 +149,6 @@ + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml index 49b0cde0..4540063a 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4CoreSubjects.cshtml @@ -188,59 +188,60 @@
-
-
- @if (HasAnyData(chartData.data)) - { - - - } - else - { -

No available data

- } -
-
- -
-
- - -
+
+ + +
+ @if (HasAnyData(chartData.data)) + { + + + } + else + { +

No available data

+ } +
+
+ +
+ + +
+
+
@@ -288,5 +289,6 @@ + } diff --git a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml index 212e946f..cf9b97e7 100644 --- a/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml +++ b/SAPSec.Web/Views/SimilarSchoolsComparison/Ks4HeadlineMeasures.cshtml @@ -24,6 +24,7 @@ + } @@ -31,132 +32,132 @@
-

Attainment 8 views

-
- @{ - var chartData = new - { - labels = new[] - { - Model.Name, - Model.SimilarSchoolName, - "Schools in England average" - }, - data = new decimal?[] - { - Model.ThisSchoolAttainment8ThreeYearAverage, - Model.SelectedSchoolAttainment8ThreeYearAverage, - Model.EnglandAttainment8ThreeYearAverage - } - }; - } -
- @if (HasAnyData(chartData.data)) - { - - - } - else - { -

No available data

- } -
-
- -
- @{ - var yearByYearData = new - { - labels = ks4YearLabels, - datasets = new object[] - { - new +
+ + + @{ + var chartData = new { - label = Model.Name, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal[] + labels = new[] { - Model.ThisSchoolAttainment8YearByYear?.Previous2 ?? 0m, - Model.ThisSchoolAttainment8YearByYear?.Previous ?? 0m, - Model.ThisSchoolAttainment8YearByYear?.Current ?? 0m - } - }, - new - { - label = Model.SimilarSchoolName, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal[] + Model.Name, + Model.SimilarSchoolName, + "Schools in England average" + }, + data = new decimal?[] { - Model.SelectedSchoolAttainment8YearByYear?.Previous2 ?? 0m, - Model.SelectedSchoolAttainment8YearByYear?.Previous ?? 0m, - Model.SelectedSchoolAttainment8YearByYear?.Current ?? 0m + Model.ThisSchoolAttainment8ThreeYearAverage, + Model.SelectedSchoolAttainment8ThreeYearAverage, + Model.EnglandAttainment8ThreeYearAverage } - }, - new + }; + } +
+ @if (HasAnyData(chartData.data)) { - label = "Schools in England average", - borderWidth = 2, - borderColor = yearByYearColors[2], - backgroundColor = yearByYearColors[2], - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal[] + + + } + else + { +

No available data

+ } +
+
+ + @{ + var yearByYearData = new + { + labels = ks4YearLabels, + datasets = new object[] { - Model.EnglandAttainment8YearByYear?.Previous2 ?? 0m, - Model.EnglandAttainment8YearByYear?.Previous ?? 0m, - Model.EnglandAttainment8YearByYear?.Current ?? 0m + new + { + label = Model.Name, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal[] + { + Model.ThisSchoolAttainment8YearByYear?.Previous2 ?? 0m, + Model.ThisSchoolAttainment8YearByYear?.Previous ?? 0m, + Model.ThisSchoolAttainment8YearByYear?.Current ?? 0m + } + }, + new + { + label = Model.SimilarSchoolName, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal[] + { + Model.SelectedSchoolAttainment8YearByYear?.Previous2 ?? 0m, + Model.SelectedSchoolAttainment8YearByYear?.Previous ?? 0m, + Model.SelectedSchoolAttainment8YearByYear?.Current ?? 0m + } + }, + new + { + label = "Schools in England average", + borderWidth = 2, + borderColor = yearByYearColors[2], + backgroundColor = yearByYearColors[2], + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal[] + { + Model.EnglandAttainment8YearByYear?.Previous2 ?? 0m, + Model.EnglandAttainment8YearByYear?.Previous ?? 0m, + Model.EnglandAttainment8YearByYear?.Current ?? 0m + } + } } - } + }; } - }; - } -
- - -
+
+ + +
+
+
@@ -225,126 +226,126 @@
-
- @{ - var engMathsChartData = new - { - labels = new[] - { - Model.Name, - Model.SimilarSchoolName, - "Schools in England average" - }, - data = new decimal?[] - { - Model.ThisSchoolEngMaths49ThreeYearAverage, - Model.SelectedSchoolEngMaths49ThreeYearAverage, - Model.EnglandEngMaths49ThreeYearAverage - } - }; - } -
- @if (HasAnyData(engMathsChartData.data)) - { - - - } - else - { -

No available data

- } -
- -
- -
- @{ - var engMathsYearByYearData = new - { - labels = ks4YearLabels, - datasets = new object[] - { - new +
+ + + @{ + var engMathsChartData = new { - label = Model.Name, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + labels = new[] { - Model.ThisSchoolEngMaths49YearByYear?.Previous2, - Model.ThisSchoolEngMaths49YearByYear?.Previous, - Model.ThisSchoolEngMaths49YearByYear?.Current - } - }, - new - { - label = Model.SimilarSchoolName, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, + Model.Name, + Model.SimilarSchoolName, + "Schools in England average" + }, data = new decimal?[] { - Model.SelectedSchoolEngMaths49YearByYear?.Previous2, - Model.SelectedSchoolEngMaths49YearByYear?.Previous, - Model.SelectedSchoolEngMaths49YearByYear?.Current + Model.ThisSchoolEngMaths49ThreeYearAverage, + Model.SelectedSchoolEngMaths49ThreeYearAverage, + Model.EnglandEngMaths49ThreeYearAverage } - }, - new + }; + } +
+ @if (HasAnyData(engMathsChartData.data)) { - label = "Schools in England average", - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + + + } + else + { +

No available data

+ } +
+
+ + @{ + var engMathsYearByYearData = new + { + labels = ks4YearLabels, + datasets = new object[] { - Model.EnglandEngMaths49YearByYear?.Previous2, - Model.EnglandEngMaths49YearByYear?.Previous, - Model.EnglandEngMaths49YearByYear?.Current + new + { + label = Model.Name, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.ThisSchoolEngMaths49YearByYear?.Previous2, + Model.ThisSchoolEngMaths49YearByYear?.Previous, + Model.ThisSchoolEngMaths49YearByYear?.Current + } + }, + new + { + label = Model.SimilarSchoolName, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.SelectedSchoolEngMaths49YearByYear?.Previous2, + Model.SelectedSchoolEngMaths49YearByYear?.Previous, + Model.SelectedSchoolEngMaths49YearByYear?.Current + } + }, + new + { + label = "Schools in England average", + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.EnglandEngMaths49YearByYear?.Previous2, + Model.EnglandEngMaths49YearByYear?.Previous, + Model.EnglandEngMaths49YearByYear?.Current + } + } } - } + }; } - }; - } -
- - -
+
+ + +
+
+
@@ -428,125 +429,126 @@
-
- @{ - var destinationsChartData = new - { - labels = new[] - { - Model.Name, - Model.SimilarSchoolName, - "Schools in England average" - }, - data = new decimal?[] - { - Model.ThisSchoolDestinationsThreeYearAverage, - Model.SelectedSchoolDestinationsThreeYearAverage, - Model.EnglandDestinationsThreeYearAverage - } - }; - } -
- @if (HasAnyData(destinationsChartData.data)) - { - - - } - else - { -

No available data

- } -
-
- -
- @{ - var destinationsYearByYearData = new - { - labels = ks4YearLabels, - datasets = new object[] - { - new +
+ + + @{ + var destinationsChartData = new { - label = Model.Name, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + labels = new[] { - Model.ThisSchoolDestinationsYearByYear?.Previous2, - Model.ThisSchoolDestinationsYearByYear?.Previous, - Model.ThisSchoolDestinationsYearByYear?.Current - } - }, - new - { - label = Model.SimilarSchoolName, - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, + Model.Name, + Model.SimilarSchoolName, + "Schools in England average" + }, data = new decimal?[] { - Model.SelectedSchoolDestinationsYearByYear?.Previous2, - Model.SelectedSchoolDestinationsYearByYear?.Previous, - Model.SelectedSchoolDestinationsYearByYear?.Current + Model.ThisSchoolDestinationsThreeYearAverage, + Model.SelectedSchoolDestinationsThreeYearAverage, + Model.EnglandDestinationsThreeYearAverage } - }, - new + }; + } +
+ @if (HasAnyData(destinationsChartData.data)) { - label = "Schools in England average", - borderWidth = 2, - pointRadius = 4, - pointHoverRadius = 5, - tension = 0, - data = new decimal?[] + + + } + else + { +

No available data

+ } +
+
+ + @{ + var destinationsYearByYearData = new + { + labels = ks4YearLabels, + datasets = new object[] { - Model.EnglandDestinationsYearByYear?.Previous2, - Model.EnglandDestinationsYearByYear?.Previous, - Model.EnglandDestinationsYearByYear?.Current + new + { + label = Model.Name, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.ThisSchoolDestinationsYearByYear?.Previous2, + Model.ThisSchoolDestinationsYearByYear?.Previous, + Model.ThisSchoolDestinationsYearByYear?.Current + } + }, + new + { + label = Model.SimilarSchoolName, + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.SelectedSchoolDestinationsYearByYear?.Previous2, + Model.SelectedSchoolDestinationsYearByYear?.Previous, + Model.SelectedSchoolDestinationsYearByYear?.Current + } + }, + new + { + label = "Schools in England average", + borderWidth = 2, + pointRadius = 4, + pointHoverRadius = 5, + tension = 0, + data = new decimal?[] + { + Model.EnglandDestinationsYearByYear?.Previous2, + Model.EnglandDestinationsYearByYear?.Previous, + Model.EnglandDestinationsYearByYear?.Current + } + } } - } + }; } - }; - } -
- - -
+
+ + +
+
+
diff --git a/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs b/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs index a504aea2..daaba6e6 100644 --- a/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs +++ b/Tests/SAPSec.UI.Tests/SchoolAttendancePageTests.cs @@ -17,6 +17,11 @@ private async Task NavigateToAttendanceAsync() await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); } + private ILocator AttendanceTabs => Page.Locator(".app-attendance-tabs"); + + private ILocator AttendanceToggleButton => + AttendanceTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }); + [Fact] public async Task Attendance_LoadsSuccessfully() { @@ -40,9 +45,10 @@ public async Task Attendance_RendersExpectedShellAndChart() await Expect(Page.Locator("#school-attendance-three-year-chart")).ToBeVisibleAsync(); await Expect(Page.Locator("#school-attendance-three-year-chart")).ToHaveAttributeAsync("data-show-no-data-labels", "true"); await Expect(Page.Locator("#school-attendance-three-year-chart")).ToHaveAttributeAsync("data-colors", "[\"#D53780\",\"#2a1950\",\"#2a1950\",\"#2a1950\"]"); - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']")).ToBeVisibleAsync(); + await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-charts']")).ToBeVisibleAsync(); await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']")).ToBeVisibleAsync(); await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-top-performers']")).ToBeVisibleAsync(); + await Expect(AttendanceTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" })).ToBeVisibleAsync(); await Expect(Page.Locator("label[for='attendanceAbsenceType']")).ToHaveClassAsync(new Regex("govuk-label--s")); await Expect(Page.Locator("#attendanceAbsenceType")).ToHaveValueAsync("overall"); } @@ -92,21 +98,24 @@ await Page.WaitForFunctionAsync( updatedDataset.Should().NotBe(initialDataset); var selectedView = Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab"); - await Expect(selectedView).ToHaveTextAsync(new Regex("3-year average")); + await Expect(selectedView).ToHaveTextAsync("Charts"); + await Expect(Page.Locator(".app-content-toggle__title")).ToHaveTextAsync("3-year average"); } [Fact] - public async Task Attendance_ChartOnlyVisibleWhenThreeYearAverageTabIsActive() + public async Task Attendance_ChartToggleSwitchesBetweenThreeYearAverageAndYearByYear() { await NavigateToAttendanceAsync(); - await Expect(Page.Locator("#attendance-three-year-average")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); + await Expect(Page.Locator(".app-content-toggle__title")).ToHaveTextAsync("3-year average"); await Expect(Page.Locator("#school-attendance-three-year-chart")).ToBeVisibleAsync(); + await Expect(Page.Locator("#attendance-year-by-year")).ToHaveAttributeAsync("hidden", "hidden"); - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']").ClickAsync(); + await AttendanceToggleButton.ClickAsync(); - await Expect(Page.Locator("#attendance-year-by-year")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - await Expect(Page.Locator("#attendance-three-year-average")).ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); + await Expect(Page.Locator(".app-content-toggle__title")).ToHaveTextAsync("Year by year"); + await Expect(Page.Locator("#school-attendance-year-by-year-chart")).ToBeVisibleAsync(); + await Expect(Page.Locator("#attendance-three-year-average")).ToHaveAttributeAsync("hidden", "hidden"); } [Fact] @@ -114,44 +123,13 @@ public async Task Attendance_TabsSwitchToYearByYearAndTable() { await NavigateToAttendanceAsync(); - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']").ClickAsync(); + await AttendanceToggleButton.ClickAsync(); await Expect(Page.Locator("#school-attendance-year-by-year-chart")).ToBeVisibleAsync(); + await Expect(AttendanceTabs.GetByRole(AriaRole.Button, new() { Name = "Show 3-year average" })).ToBeVisibleAsync(); var tableTab = Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']"); await tableTab.ClickAsync(); await Expect(Page.Locator("#attendance-table .govuk-table")).ToBeVisibleAsync(); await Expect(Page.Locator("#attendance-table .govuk-table")).ToContainTextAsync("Similar schools average"); } - - [Fact] - public async Task Attendance_TopPerformersTab_RendersExpectedContent_AndUpdatesForPersistentAbsence() - { - await NavigateToAttendanceAsync(); - - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-top-performers']").ClickAsync(); - - await Expect(Page.Locator("#attendance-top-performers")).ToContainTextAsync( - "These are the top performing similar schools for this measure."); - await Expect(Page.Locator("#attendance-top-performers-table thead")).ToContainTextAsync("Rank"); - await Expect(Page.Locator("#attendance-top-performers-table thead")).ToContainTextAsync("School"); - await Expect(Page.Locator("#attendance-top-performers-table thead")).ToContainTextAsync("3-year average"); - await Expect(Page.Locator("#attendance-top-performers-table tbody tr")).ToHaveCountAsync(3); - await Expect(Page.Locator("#attendance-top-performers-table tbody tr").Nth(0)).ToContainTextAsync("1"); - await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "See all similar schools" })) - .ToHaveAttributeAsync("href", "/school/145327/view-similar-schools"); - - var initialValues = await Page.Locator("#attendance-top-performers-table tbody [data-top-performer-value], #attendance-top-performers-table tbody .govuk-table__cell--numeric") - .AllTextContentsAsync(); - - await Page.SelectOptionAsync("#attendanceAbsenceType", "persistent"); - await Expect(Page.Locator("#attendanceAbsenceType")).ToHaveValueAsync("persistent"); - await Expect(Page.Locator("#attendance-top-performers-table tbody tr")).ToHaveCountAsync(3); - - await Page.WaitForFunctionAsync( - @"([selector, initial]) => { - const values = Array.from(document.querySelectorAll(selector)).map(x => x.textContent.trim()); - return JSON.stringify(values) !== initial; - }", - new object[] { "#attendance-top-performers-table tbody .govuk-table__cell--numeric", System.Text.Json.JsonSerializer.Serialize(initialValues) }); - } } diff --git a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs index 5092ebc8..0d75b54d 100644 --- a/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SchoolKs4HeadlineMeasuresPageTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using Microsoft.Playwright; using SAPSec.UI.Tests.Infrastructure; +using System.Linq; +using System.Text.Json; using Xunit; namespace SAPSec.UI.Tests; @@ -17,6 +19,12 @@ private async Task NavigateAsync() await Expect(Page.Locator("#ks4-attainment8-school-chart")).ToBeVisibleAsync(); } + private async Task ToggleChartViewAsync(int chartGroupIndex = 0) + { + var chartTabs = Page.Locator(".app-ks4-tabs").Nth(chartGroupIndex); + await chartTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }).ClickAsync(); + } + [Fact] public async Task Ks4HeadlineMeasures_LoadsSuccessfully() { @@ -36,7 +44,7 @@ public async Task Ks4HeadlineMeasures_UsesExpectedColoursForAttainmentCharts() var lineChart = Page.Locator("#ks4-attainment8-school-yearbyyear-chart"); await Expect(barChart).ToBeVisibleAsync(); - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); + await ToggleChartViewAsync(); await Expect(lineChart).ToBeVisibleAsync(); var barColours = await barChart.EvaluateAsync(@" @@ -62,18 +70,29 @@ public async Task Ks4HeadlineMeasures_Attainment8YearByYear_ShowsExpectedYAxisTi { await NavigateAsync(); - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); + await ToggleChartViewAsync(); var lineChart = Page.Locator("#ks4-attainment8-school-yearbyyear-chart"); await Expect(lineChart).ToBeVisibleAsync(); - var tickLabels = await lineChart.EvaluateAsync(@" + var axis = await lineChart.EvaluateAsync(@" el => { const chart = window.Chart && window.Chart.getChart(el); - return chart?.scales?.y?.ticks?.map(tick => tick.label) ?? []; + return { + min: chart?.scales?.y?.min ?? null, + max: chart?.scales?.y?.max ?? null, + ticks: chart?.scales?.y?.ticks?.map(tick => tick.label) ?? [] + }; } "); - tickLabels.Should().Equal("0", "30", "60", "90"); + var min = axis.GetProperty("min").GetDouble(); + var max = axis.GetProperty("max").GetDouble(); + var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); + + min.Should().BeGreaterThan(0d); + max.Should().BeGreaterThan(min); + (max - min).Should().BeLessThan(90d); + ticks.Should().HaveCountGreaterThan(1); } [Fact] @@ -101,9 +120,9 @@ public async Task Ks4HeadlineMeasures_UsesExpectedColoursForAllSchoolCharts() colours.Should().Equal("#ca357c", "#2a1950", "#2a1950", "#2a1950"); } - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); - await Page.Locator(".govuk-tabs__tab[href='#eng-maths-year-by-year']").ClickAsync(); - await Page.Locator(".govuk-tabs__tab[href='#destinations-year-by-year']").ClickAsync(); + await ToggleChartViewAsync(0); + await ToggleChartViewAsync(1); + await ToggleChartViewAsync(2); var lineChartSelectors = new[] { diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs deleted file mode 100644 index 0671332c..00000000 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonAttendancePageTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FluentAssertions; -using Microsoft.Playwright; -using SAPSec.UI.Tests.Infrastructure; -using System.Text.RegularExpressions; -using Xunit; - -namespace SAPSec.UI.Tests; - -[Collection("UITestsCollection")] -public class SimilarSchoolsComparisonAttendancePageTests(WebApplicationSetupFixture fixture) : BasePageTest(fixture) -{ - private const string Path = "/school/108088/view-similar-schools/137621/Attendance"; - - [Fact] - public async Task AttendanceComparison_LoadsWithExpectedDefaults() - { - var response = await Page.GotoAsync(Path); - - response.Should().NotBeNull(); - response!.Status.Should().Be(200); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - - await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Attendance measures" })).ToBeVisibleAsync(); - await Expect(Page.Locator("label[for='attendanceAbsenceType']")).ToHaveClassAsync(new Regex("govuk-label--s")); - - var absenceType = Page.Locator("#attendanceAbsenceType"); - await Expect(absenceType).ToHaveValueAsync("overall"); - - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-three-year-average']")).ToBeVisibleAsync(); - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']")).ToBeVisibleAsync(); - await Expect(Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']")).ToBeVisibleAsync(); - - await Expect(Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab")).ToHaveTextAsync("3-year average"); - } - - [Fact] - public async Task AttendanceComparison_TabsSwitchByClickAndKeyboard() - { - await Page.GotoAsync(Path); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - - await Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-year-by-year']").ClickAsync(); - await Expect(Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab")).ToHaveTextAsync("Year by year"); - await Expect(Page.Locator("#attendance-year-by-year")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - - var tableTab = Page.Locator(".app-attendance-tabs .govuk-tabs__tab[href='#attendance-table']"); - await tableTab.FocusAsync(); - await tableTab.PressAsync("Space"); - await Expect(Page.Locator(".govuk-tabs__list-item--selected .govuk-tabs__tab")).ToHaveTextAsync("Table"); - await Expect(Page.Locator("#attendance-table")).Not.ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - await Expect(Page.Locator("#attendance-year-by-year")).ToHaveClassAsync(new Regex("govuk-tabs__panel--hidden")); - } -} diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs index d49bb57a..3e36ee2e 100644 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4CoreSubjectsPageTests.cs @@ -10,6 +10,12 @@ public class SimilarSchoolsComparisonKs4CoreSubjectsPageTests(WebApplicationSetu { private const string Path = "/school/108088/view-similar-schools/137621/Ks4CoreSubjects"; + private async Task ToggleFirstChartGroupToYearByYearAsync() + { + var chartTabs = Page.Locator(".app-ks4-tabs").First; + await chartTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }).ClickAsync(); + } + [Fact] public async Task Ks4CoreSubjectsComparison_LoadsSuccessfully() { @@ -46,9 +52,13 @@ public async Task Ks4CoreSubjectsComparison_YearByYearChartsUsePercentAxisInQuar await Page.GotoAsync(Path); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await ToggleFirstChartGroupToYearByYearAsync(); + var lineCharts = Page.Locator("canvas[id$='-comparison-yearbyyear-chart'][data-type='line']"); await Expect(lineCharts).ToHaveCountAsync(7); + await Expect(Page.Locator("#english-language-comparison-yearbyyear-chart")).ToBeVisibleAsync(); + for (var index = 0; index < await lineCharts.CountAsync(); index++) { var chart = lineCharts.Nth(index); @@ -63,6 +73,7 @@ public async Task Ks4CoreSubjectsComparison_YearByYearChartsUsePercentAxisInQuar .Locator("#english-language-comparison-yearbyyear-chart") .EvaluateAsync("canvas => window.Chart.getChart(canvas).scales.y.ticks.map(tick => tick.label)"); - englishLanguageTickLabels.Should().Equal("0%", "25%", "50%", "75%", "100%"); + englishLanguageTickLabels.Should().NotBeEmpty(); + englishLanguageTickLabels.Should().OnlyContain(label => label != null && label.EndsWith("%")); } } diff --git a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs index 7d4bf94d..b54a39ba 100644 --- a/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs +++ b/Tests/SAPSec.UI.Tests/SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using Microsoft.Playwright; using SAPSec.UI.Tests.Infrastructure; +using System.Linq; +using System.Text.Json; using Xunit; namespace SAPSec.UI.Tests; @@ -10,6 +12,12 @@ public class SimilarSchoolsComparisonKs4HeadlineMeasuresPageTests(WebApplication { private const string Path = "/school/108088/view-similar-schools/137621/Ks4HeadlineMeasures"; + private async Task ToggleChartViewAsync(int chartGroupIndex = 0) + { + var chartTabs = Page.Locator(".app-ks4-tabs").Nth(chartGroupIndex); + await chartTabs.GetByRole(AriaRole.Button, new() { Name = "Show year by year" }).ClickAsync(); + } + [Fact] public async Task Ks4HeadlineMeasuresComparison_LoadsSuccessfully() { @@ -47,17 +55,28 @@ public async Task Ks4HeadlineMeasuresComparison_Attainment8YearByYear_ShowsExpec await Page.GotoAsync(Path); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - await Page.Locator(".govuk-tabs__tab[href='#year-by-year']").ClickAsync(); + await ToggleChartViewAsync(); var lineChart = Page.Locator("#ks4-attainment8-comparison-yearbyyear-chart"); await Expect(lineChart).ToBeVisibleAsync(); - var tickLabels = await lineChart.EvaluateAsync(@" + var axis = await lineChart.EvaluateAsync(@" el => { const chart = window.Chart && window.Chart.getChart(el); - return chart?.scales?.y?.ticks?.map(tick => tick.label) ?? []; + return { + min: chart?.scales?.y?.min ?? null, + max: chart?.scales?.y?.max ?? null, + ticks: chart?.scales?.y?.ticks?.map(tick => tick.label) ?? [] + }; } "); - tickLabels.Should().Equal("0", "30", "60", "90"); + var min = axis.GetProperty("min").GetDouble(); + var max = axis.GetProperty("max").GetDouble(); + var ticks = axis.GetProperty("ticks").EnumerateArray().Select(tick => tick.GetString()).ToArray(); + + min.Should().BeGreaterThan(0d); + max.Should().BeGreaterThan(min); + (max - min).Should().BeLessThan(90d); + ticks.Should().HaveCountGreaterThan(1); } } diff --git a/terraform/application/config/review.yml b/terraform/application/config/review.yml index fff19cc8..ecc87cc9 100644 --- a/terraform/application/config/review.yml +++ b/terraform/application/config/review.yml @@ -1,3 +1,4 @@ --- ASPNETCORE_ENVIRONMENT: "Development" FeatureManagement__EnablePrimarySchools: "true" +reset_review_db: "true"