diff --git a/.github/workflows/code-standards.yaml b/.github/workflows/code-standards.yaml index 13765e552..9baf22e32 100644 --- a/.github/workflows/code-standards.yaml +++ b/.github/workflows/code-standards.yaml @@ -295,6 +295,36 @@ jobs: - name: run tests run: npx playwright test a11y + new-end-to-end-tests: + name: playwright end to end tests + runs-on: ubuntu-latest + needs: [populate-database, should-test] + + steps: + - uses: browser-actions/setup-chrome@v1 + - uses: browser-actions/setup-edge@v1 + + - name: checkout + if: needs.should-test.outputs.yes == 'true' + uses: actions/checkout@v4 + + - name: setup site + if: needs.should-test.outputs.yes == 'true' + uses: ./.github/actions/setup-site + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: install dependencies + run: npm ci + + - name: install browsers + run: npx playwright install --with-deps + + - name: run tests + run: npx playwright test e2e/* + end-to-end-tests: name: end-to-end tests runs-on: ubuntu-latest diff --git a/package.json b/package.json index fa87dfbc7..1635a4931 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "scripts": { "load-spatial": "cd spatial-data && node load-shapefiles.js", "cypress:e2e": "cypress open --project tests/e2e", + "a11y": "playwright test a11y", + "playwright:e2e": "playwright test e2e/*", "js-component-tests": "npx mocha web/themes/**/tests/components/*-tests.js", "js-format": "npx prettier -w 'web/themes/**/assets/**/*.js' 'tests/**/*.js' '*.js'", "js-lint": "eslint 'web/**/assets/**/*.js' 'tests/**/*.js' '*.js'", diff --git a/tests/api/data/e2e/alerts/active__status=actual&area=AR.json b/tests/api/data/e2e/alerts/active__status=actual&area=AR.json index c534ba477..fc85f241b 100644 --- a/tests/api/data/e2e/alerts/active__status=actual&area=AR.json +++ b/tests/api/data/e2e/alerts/active__status=actual&area=AR.json @@ -245,7 +245,7 @@ "sender": "w-nws.webmaster@noaa.gov", "senderName": "NWS Los Angeles/Oxnard CA", "headline": "High Wind Warning issued January 3 at 9:50AM PST until January 5 at 1:00AM PST by NWS Los Angeles/Oxnard CA", - "description": "* WHAT...North winds 25 to 35 mph with gusts up to 60 to 65 mph\nexpected.\n\n* WHERE...Santa Ynez Mountains Eastern Range.\n\n* WHEN...Until 1 AM PST Friday.\n\n* IMPACTS...Damaging winds will blow down large objects such as\ntrees and power lines. Power outages are expected. Travel will\nbe difficult, especially for high profile vehicles.", + "description": "* WHAT...North winds 25 to 35 mph with gusts up to 60 to 65 mph\nexpected.\n\n* WHERE...Santa Ynez Mountains Eastern Range.\n\n* WHEN...Until 1 AM PST Friday.\n\n* IMPACTS...Damaging winds will blow down large objects such as\ntrees and power lines. Power outages are expected. See www.your-power-company.com/outages for more information. Travel will\nbe difficult, especially for high profile vehicles. For road safety, see https://transportation.gov/safe-travels . For more weather information, check out https://weather.gov/your-office for up to date forecasts and alerts.", "instruction": "People should avoid being outside in forested areas and around\ntrees and branches. If possible, remain in the lower levels of\nyour home during the windstorm, and avoid windows. Use caution if\nyou must drive.", "response": "Prepare", "parameters": { diff --git a/tests/api/data/testing/alerts/active__status=actual&area=AR.json b/tests/api/data/testing/alerts/active__status=actual&area=AR.json index a38946c02..fc85f241b 100644 --- a/tests/api/data/testing/alerts/active__status=actual&area=AR.json +++ b/tests/api/data/testing/alerts/active__status=actual&area=AR.json @@ -19,68 +19,16 @@ "id": "urn:oid:2.49.0.1.840.0.35c08d67b727d304ea1d6ae50eb6139b4be1cc8f.001.1", "areaDesc": "San Francisco; Marin Coastal Range; Sonoma Coastal Range; North Bay Interior Mountains; Coastal North Bay Including Point Reyes National Seashore; North Bay Interior Valleys; San Francisco Bay Shoreline; San Fransisco Peninsula Coast; East Bay Interior Valleys; Santa Cruz Mountains; Santa Clara Valley Including San Jose; Eastern Santa Clara Hills; East Bay Hills; Southern Salinas Valley/Arroyo Seco and Lake San Antonio; Santa Lucia Mountains and Los Padres National Forest; Mountains Of San Benito County And Interior Monterey County Including Pinnacles National Park; Northern Salinas Valley/Hollister Valley and Carmel Valley; Northern Monterey Bay; Southern Monterey Bay and Big Sur Coast", "geocode": { - "SAME": [ - "006075", - "006041", - "006097", - "006055", - "006001", - "006013", - "006081", - "006085", - "006087", - "006053", - "006069" - ], - "UGC": [ - "CAZ006", - "CAZ502", - "CAZ503", - "CAZ504", - "CAZ505", - "CAZ506", - "CAZ508", - "CAZ509", - "CAZ510", - "CAZ512", - "CAZ513", - "CAZ514", - "CAZ515", - "CAZ516", - "CAZ517", - "CAZ518", - "CAZ528", - "CAZ529", - "CAZ530" - ] + "SAME": [], + "UGC": [] }, - "affectedZones": [ - "https://api.weather.gov/zones/forecast/CAZ006", - "https://api.weather.gov/zones/forecast/CAZ502", - "https://api.weather.gov/zones/forecast/CAZ503", - "https://api.weather.gov/zones/forecast/CAZ504", - "https://api.weather.gov/zones/forecast/CAZ505", - "https://api.weather.gov/zones/forecast/CAZ506", - "https://api.weather.gov/zones/forecast/CAZ508", - "https://api.weather.gov/zones/forecast/CAZ509", - "https://api.weather.gov/zones/forecast/CAZ510", - "https://api.weather.gov/zones/forecast/CAZ512", - "https://api.weather.gov/zones/forecast/CAZ513", - "https://api.weather.gov/zones/forecast/CAZ514", - "https://api.weather.gov/zones/forecast/CAZ515", - "https://api.weather.gov/zones/forecast/CAZ516", - "https://api.weather.gov/zones/forecast/CAZ517", - "https://api.weather.gov/zones/forecast/CAZ518", - "https://api.weather.gov/zones/forecast/CAZ528", - "https://api.weather.gov/zones/forecast/CAZ529", - "https://api.weather.gov/zones/forecast/CAZ530" - ], + "affectedZones": ["https://api.weather.gov/zones/forecast/ARZ045"], "references": [], "sent": "2024-01-29T12:41:00-08:00", - "effective": "2024-01-29T12:41:00-08:00", - "onset": "2024-01-31T04:00:00-08:00", - "expires": "2024-01-30T03:00:00-08:00", - "ends": "2024-02-02T04:00:00-08:00", + "effective": "date:now -1 hour", + "onset": "date:now -1 hour", + "expires": "date:now +1 hour", + "ends": "date:now +1 hour", "status": "Actual", "messageType": "Alert", "category": "Met", @@ -120,13 +68,16 @@ "SAME": ["006035", "006063", "006091"], "UGC": ["CAZ071"] }, - "affectedZones": ["https://api.weather.gov/zones/forecast/CAZ071"], + "affectedZones": [ + "https://api.weather.gov/zones/forecast/CAZ071", + "https://api.weather.gov/zones/fire/ARZ046" + ], "references": [], "sent": "2024-01-29T13:16:00-08:00", - "effective": "2024-01-29T13:16:00-08:00", - "onset": "2024-01-31T10:00:00-08:00", - "expires": "2024-01-30T13:30:00-08:00", - "ends": "2024-02-02T10:00:00-08:00", + "effective": "date:now -1 hour", + "onset": "date:now -1 hour", + "expires": "date:now +1 hour", + "ends": "date:now +1 hour", "status": "Actual", "messageType": "Alert", "category": "Met", @@ -156,134 +107,7 @@ { "id": "https://api.weather.gov/alerts/urn:oid:2.49.0.1.840.0.550760f4ae698a45e239ca34a1ad97353d170175.001.1", "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [-92.275, 34.999], - [-92.26191601093927, 34.998657383688645], - [-92.2488678841831, 34.99763047384207], - [-92.23589138373995, 34.995922085148784], - [-92.22302207729557, 34.99353690018346], - [-92.21029523872437, 34.99048145657227], - [-92.19774575140627, 34.98676412907379], - [-92.18540801261368, 34.9823951066243], - [-92.17331583923105, 34.977386364410656], - [-92.16150237506511, 34.97175163104709], - [-92.15, 34.965506350946114], - [-92.13884024124624, 34.95866764198636], - [-92.12805368692689, 34.95125424859374], - [-92.11766990223755, 34.943286490364244], - [-92.10771734841029, 34.93478620636935], - [-92.09822330470337, 34.92577669529664], - [-92.08921379363066, 34.91628265158972], - [-92.08071350963576, 34.90633009776246], - [-92.07274575140627, 34.89594631307312], - [-92.06533235801365, 34.88515975875376], - [-92.0584936490539, 34.874], - [-92.05224836895292, 34.86249762493489], - [-92.04661363558935, 34.85068416076895], - [-92.0416048933757, 34.83859198738633], - [-92.03723587092622, 34.82625424859374], - [-92.03351854342773, 34.81370476127563], - [-92.03046309981656, 34.80097792270444], - [-92.02807791485122, 34.78810861626006], - [-92.02636952615794, 34.775132115816916], - [-92.02534261631136, 34.76208398906074], - [-92.025, 34.749], - [-92.02534261631136, 34.73591601093926], - [-92.02636952615794, 34.72286788418309], - [-92.02807791485122, 34.709891383739944], - [-92.03046309981656, 34.697022077295564], - [-92.03351854342773, 34.684295238724374], - [-92.03723587092622, 34.67174575140626], - [-92.0416048933757, 34.659408012613675], - [-92.04661363558935, 34.647315839231055], - [-92.05224836895292, 34.635502375065116], - [-92.0584936490539, 34.624], - [-92.06533235801365, 34.612840241246246], - [-92.07274575140627, 34.60205368692689], - [-92.08071350963576, 34.59166990223754], - [-92.08921379363066, 34.58171734841029], - [-92.09822330470337, 34.572223304703364], - [-92.10771734841029, 34.56321379363065], - [-92.11766990223755, 34.55471350963576], - [-92.12805368692689, 34.54674575140626], - [-92.13884024124624, 34.539332358013645], - [-92.15, 34.53249364905389], - [-92.16150237506511, 34.52624836895291], - [-92.17331583923105, 34.52061363558935], - [-92.18540801261368, 34.5156048933757], - [-92.19774575140627, 34.511235870926214], - [-92.21029523872437, 34.507518543427736], - [-92.22302207729557, 34.50446309981655], - [-92.23589138373995, 34.50207791485122], - [-92.2488678841831, 34.50036952615793], - [-92.26191601093927, 34.49934261631136], - [-92.275, 34.499], - [-92.28808398906074, 34.49934261631136], - [-92.30113211581691, 34.50036952615793], - [-92.31410861626006, 34.50207791485122], - [-92.32697792270444, 34.50446309981655], - [-92.33970476127564, 34.507518543427736], - [-92.35225424859374, 34.511235870926214], - [-92.36459198738633, 34.5156048933757], - [-92.37668416076896, 34.52061363558935], - [-92.38849762493489, 34.52624836895291], - [-92.4, 34.53249364905389], - [-92.41115975875377, 34.539332358013645], - [-92.42194631307312, 34.54674575140626], - [-92.43233009776246, 34.55471350963576], - [-92.44228265158972, 34.56321379363065], - [-92.45177669529664, 34.572223304703364], - [-92.46078620636935, 34.58171734841029], - [-92.46928649036425, 34.59166990223754], - [-92.47725424859374, 34.60205368692689], - [-92.48466764198636, 34.612840241246246], - [-92.49150635094611, 34.624], - [-92.4977516310471, 34.635502375065116], - [-92.50338636441066, 34.647315839231055], - [-92.5083951066243, 34.659408012613675], - [-92.51276412907379, 34.67174575140626], - [-92.51648145657228, 34.684295238724374], - [-92.51953690018345, 34.697022077295564], - [-92.52192208514879, 34.709891383739944], - [-92.52363047384208, 34.72286788418309], - [-92.52465738368865, 34.73591601093926], - [-92.525, 34.749], - [-92.52465738368865, 34.76208398906074], - [-92.52363047384208, 34.775132115816916], - [-92.52192208514879, 34.78810861626006], - [-92.51953690018345, 34.80097792270444], - [-92.51648145657228, 34.81370476127563], - [-92.51276412907379, 34.82625424859374], - [-92.5083951066243, 34.83859198738633], - [-92.50338636441066, 34.85068416076895], - [-92.4977516310471, 34.86249762493489], - [-92.49150635094611, 34.874], - [-92.48466764198636, 34.88515975875376], - [-92.47725424859374, 34.89594631307312], - [-92.46928649036425, 34.90633009776246], - [-92.46078620636935, 34.91628265158972], - [-92.45177669529664, 34.92577669529664], - [-92.44228265158972, 34.93478620636935], - [-92.43233009776246, 34.943286490364244], - [-92.42194631307312, 34.95125424859374], - [-92.41115975875377, 34.95866764198636], - [-92.4, 34.965506350946114], - [-92.3884976249349, 34.97175163104709], - [-92.37668416076896, 34.977386364410656], - [-92.36459198738633, 34.9823951066243], - [-92.35225424859374, 34.98676412907379], - [-92.33970476127564, 34.99048145657227], - [-92.32697792270444, 34.99353690018345], - [-92.31410861626006, 34.995922085148784], - [-92.30113211581691, 34.99763047384207], - [-92.28808398906074, 34.998657383688645], - [-92.275, 34.999] - ] - ] - }, + "geometry": null, "properties": { "@id": "https://api.weather.gov/alerts/urn:oid:2.49.0.1.840.0.550760f4ae698a45e239ca34a1ad97353d170175.001.1", "@type": "wx:Alert", @@ -293,7 +117,7 @@ "SAME": ["005119"], "UGC": [] }, - "affectedZones": [], + "affectedZones": ["https://api.weather.gov/zones/county/ARC120"], "references": [ { "@id": "https://api.weather.gov/alerts/urn:oid:2.49.0.1.840.0.1afb9e8030b0ed3aac216a52624965e145929bc4.003.1", @@ -345,7 +169,7 @@ "id": "urn:oid:2.49.0.1.840.0.8760a86c78e313ccfc42aa4eb5166572a0e26e9d.003.1", "areaDesc": "Eastern Beaufort Sea Coast", "geocode": { - "SAME": ["005119"], + "SAME": ["005119", "005141"], "UGC": [] }, "affectedZones": [], @@ -357,11 +181,11 @@ "sent": "2024-01-03T02:50:00-09:00" } ], - "sent": "date:now -1 hour", - "effective": "date:now -24 hour", - "onset": "date:now +24 hour", - "expires": "date:now +40 hour", - "ends": "date:now +48 hour", + "sent": "date:today 16:00:-5 -24 hours", + "effective": "date:today 16:00:-5 +24 hours", + "onset": "date:today 16:00:-5 +24 hours", + "expires": "date:today 16:00:-5 +40 hours", + "ends": "date:today 16:00:-5 +48 hours", "status": "Actual", "messageType": "Update", "category": "Met", @@ -421,7 +245,7 @@ "sender": "w-nws.webmaster@noaa.gov", "senderName": "NWS Los Angeles/Oxnard CA", "headline": "High Wind Warning issued January 3 at 9:50AM PST until January 5 at 1:00AM PST by NWS Los Angeles/Oxnard CA", - "description": "* WHAT...North winds 25 to 35 mph with gusts up to 60 to 65 mph\nexpected.\n\n* WHERE...Santa Ynez Mountains Eastern Range.\n\n* WHEN...Until 1 AM PST Friday.\n\n* IMPACTS...Damaging winds will blow down large objects such as\ntrees and power lines. Power outages are expected. Travel will\nbe difficult, especially for high profile vehicles.", + "description": "* WHAT...North winds 25 to 35 mph with gusts up to 60 to 65 mph\nexpected.\n\n* WHERE...Santa Ynez Mountains Eastern Range.\n\n* WHEN...Until 1 AM PST Friday.\n\n* IMPACTS...Damaging winds will blow down large objects such as\ntrees and power lines. Power outages are expected. See www.your-power-company.com/outages for more information. Travel will\nbe difficult, especially for high profile vehicles. For road safety, see https://transportation.gov/safe-travels . For more weather information, check out https://weather.gov/your-office for up to date forecasts and alerts.", "instruction": "People should avoid being outside in forested areas and around\ntrees and branches. If possible, remain in the lower levels of\nyour home during the windstorm, and avoid windows. Use caution if\nyou must drive.", "response": "Prepare", "parameters": { @@ -553,10 +377,10 @@ "id": "urn:oid:2.49.0.1.840.0.edf7c693eacb6f6c34f207d231789f1984baa629.036.2", "areaDesc": "Saint Matthew Island Waters", "geocode": { - "SAME": ["005119"], + "SAME": [], "UGC": [] }, - "affectedZones": [], + "affectedZones": ["https://api.weather.gov/zones/forecast/AR-MARINE"], "references": [], "sent": "date:now -1 hour", "effective": "date:now -1 hour", @@ -755,6 +579,69 @@ ] } } + }, + { + "id": "https://api.weather.gov/alerts/urn:oid:2.49.0.1.840.0.75fb8046440f816bf90ca0e70a8b3b6e493b7985.001.1", + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-92, 30], + [-90.5, 30], + [-90.5, 30.5], + [-92, 30.5], + [-92, 30] + ] + ] + }, + "properties": { + "@id": "https://api.weather.gov/alerts/urn:oid:2.49.0.1.840.0.75fb8046440f816bf90ca0e70a8b3b6e493b7985.001.1", + "@type": "wx:Alert", + "id": "urn:oid:2.49.0.1.840.0.75fb8046440f816bf90ca0e70a8b3b6e493b7985.001.1", + "areaDesc": "Appling, GA; Bacon, GA; Wayne, GA", + "geocode": { + "SAME": [], + "UGC": [] + }, + "affectedZones": [], + "references": [], + "sent": "date:now -7 minutes", + "effective": "date:now -7 minutes", + "onset": "date:now -7 minutes", + "expires": "date:now +14 minutes", + "ends": null, + "status": "Actual", + "messageType": "Alert", + "category": "Met", + "severity": "Severe", + "certainty": "Observed", + "urgency": "Immediate", + "event": "Severe Thunderstorm Warning", + "sender": "w-nws.webmaster@noaa.gov", + "senderName": "NWS Jacksonville FL", + "headline": "Severe Thunderstorm Warning issued January 9 at 1:48PM EST until January 9 at 2:15PM EST by NWS Jacksonville FL", + "description": "The National Weather Service in Jacksonville has issued a\n\n* Severe Thunderstorm Warning for...\nSoutheastern Bacon County in southeastern Georgia...\nNorthwestern Wayne County in southeastern Georgia...\nSoutheastern Appling County in southeastern Georgia...\n\n* Until 215 PM EST.\n\n* At 148 PM EST, severe thunderstorms were located along a line\nextending from 7 miles southwest of Glennville to 6 miles northeast\nof Bristol to near Blackshear, moving northeast at 60 mph.\n\nHAZARD...70 mph wind gusts and penny size hail.\n\nSOURCE...Radar indicated.\n\nIMPACT...Expect considerable tree damage. Damage is likely to\nmobile homes, roofs, and outbuildings.\n\n* Locations impacted include...\nSurrency, Odum, and New Lacy.", + "instruction": "Remain alert for a possible tornado! Tornadoes can develop quickly\nfrom severe thunderstorms. If you spot a tornado go at once into the\nbasement or small central room in a sturdy structure.\n\nFor your protection move to an interior room on the lowest floor of a\nbuilding.\n\nIntense thunderstorm lines can produce brief tornadoes and widespread\nsignificant wind damage. Although a tornado is not immediately\nlikely, it is best to move to an interior room on the lowest floor of\na building. These storms may cause serious injury and significant\nproperty damage.\n\nA Tornado Watch remains in effect until 300 PM EST for southeastern\nGeorgia. A Tornado Watch also remains in effect until 600 PM EST for\nsoutheastern Georgia.", + "response": "Shelter", + "parameters": { + "AWIPSidentifier": ["SVRJAX"], + "WMOidentifier": ["WUUS52 KJAX 091848"], + "eventMotionDescription": [ + "2024-01-09T18:48:00-00:00...storm...238DEG...52KT...31.86,-82.01 31.53,-82.17 31.36,-82.3" + ], + "windThreat": ["RADAR INDICATED"], + "maxWindGust": ["70 MPH"], + "hailThreat": ["RADAR INDICATED"], + "maxHailSize": ["0.75"], + "thunderstormDamageThreat": ["CONSIDERABLE"], + "tornadoDetection": ["POSSIBLE"], + "BLOCKCHANNEL": ["EAS", "NWEM", "CMAS"], + "EAS-ORG": ["WXR"], + "VTEC": ["/O.NEW.KJAX.SV.W.0011.240109T1848Z-240109T1915Z/"], + "eventEndingTime": ["2024-01-09T19:15:00+00:00"] + } + } } ], "title": "Current watches, warnings, and advisories for 44.9869 N, 93.2926 W", diff --git a/tests/e2e/cypress/e2e/alerts.cy.js b/tests/e2e/cypress/e2e/alerts.cy.js deleted file mode 100644 index 1897370a2..000000000 --- a/tests/e2e/cypress/e2e/alerts.cy.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint no-unused-expressions: off */ -describe("Alert display testing", () => { - beforeEach(() => { - cy.request("http://localhost:8081/play/testing"); - cy.visit("/point/34.749/-92.275"); - }); - - it("The correct number of alerts show on the page", () => { - cy.get("weathergov-alerts").find("div.usa-accordion").should("have.length", 7); - }); - - it("All alert accordions are open by default", () => { - cy.get("weathergov-alerts > div.usa-accordion button") - .invoke("attr", "aria-expanded") - .should("equal", "true"); - }); - - it("Clicking the alert accordion buttons closes them", () => { - cy.get("weathergov-alerts > div.usa-accordion button").as("accordionButton") - .click({multiple: true}); - cy.get("@accordionButton").invoke("attr", "aria-expanded").should("equal", "false"); - }); -}); diff --git a/tests/playwright/e2e/alerts.spec.js b/tests/playwright/e2e/alerts.spec.js new file mode 100644 index 000000000..74ce85322 --- /dev/null +++ b/tests/playwright/e2e/alerts.spec.js @@ -0,0 +1,59 @@ +/* eslint-disable no-await-in-loop, no-plusplus */ +const { test, expect } = require("@playwright/test"); + +const { describe, beforeEach } = test; + +describe("Alerts e2e tests", () => { + beforeEach(async ({ page }) => { + await page.goto("http://localhost:8081/play/testing"); + await page.goto("/point/34.749/-92.275"); + }); + + test("The correct number of alerts show on the page", async ({ page }) => { + const alertAccordions = await page.locator("weathergov-alerts div.usa-accordion").all(); + expect(alertAccordions).toHaveLength(6); + }); + + test("All alert accordions are open by default", async ({ page }) => { + const alertAccordions = await page.locator('weathergov-alerts div.usa-accordion button[aria-expanded="true"]').all(); + expect(alertAccordions).toHaveLength(6); + }); + + test("Clicking the alert accordion buttons closes them", async ({ page }) => { + const alertAccordions = page.locator("weathergov-alerts div.usa-accordion button"); + for(let i = 0; i < await alertAccordions.count(); i++){ + await alertAccordions.nth(i).click(); + await expect(alertAccordions.nth(i)).toHaveAttribute("aria-expanded", "false"); + } + }); + + describe("Parsed URLs in alerts", () => { + test("Should not find a link wrapping the non-gov url", async ({ page }) => { + const containingText = page.locator("weathergov-alerts").getByText("www.your-power-company.com/outages"); + await expect(containingText).toHaveCount(1); + + const link = page.locator('a[href="www.your-power-company.com/outages"]'); + await expect(link).toHaveCount(0); + }); + + test("Should find a link wrapping the external url, and should have the correct class", async ({ page }) => { + const containingText = page.locator("weathergov-alerts").getByText("https://transportation.gov/safe-travels"); + await expect(containingText).toHaveCount(1); + + const link = page.getByRole("link").filter({hasText: "https://transportation.gov/safe-travels"}); + await expect(link).toHaveCount(1); + await expect(link).toHaveAttribute("href", "https://transportation.gov/safe-travels"); + await expect(link).toHaveClass(/usa-link--external/); + }); + + test("Should find a link wrapping the inernal url, and should have the correct class", async ({ page }) => { + const containingText = page.locator("weathergov-alerts").getByText("https://weather.gov/your-office"); + await expect(containingText).toHaveCount(1); + + const link = page.getByRole("link").filter({hasText: "https://weather.gov/your-office"}); + await expect(link).toHaveCount(1); + await expect(link).toHaveAttribute("href", "https://weather.gov/your-office"); + await expect(link).not.toHaveClass(/usa-link--external/); + }); + }); +}); diff --git a/web/modules/weather_data/src/Service/Test/WeatherAlertParser.php.test b/web/modules/weather_data/src/Service/Test/WeatherAlertParser.php.test index ca3a0d792..97a849b3c 100644 --- a/web/modules/weather_data/src/Service/Test/WeatherAlertParser.php.test +++ b/web/modules/weather_data/src/Service/Test/WeatherAlertParser.php.test @@ -30,8 +30,13 @@ final class WeatherAlertParserTest extends TestCase ], [ "type" => "paragraph", - "text" => - "Snow expected. Total snow accumulations of 5 to 10 inches.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Snow expected. Total snow accumulations of 5 to 10 inches.", + ], + ], ], [ "type" => "heading", @@ -39,7 +44,13 @@ final class WeatherAlertParserTest extends TestCase ], [ "type" => "paragraph", - "text" => "Eastern San Juan Mountains Above 10000 Feet.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Eastern San Juan Mountains Above 10000 Feet.", + ], + ], ], [ "type" => "heading", @@ -47,7 +58,13 @@ final class WeatherAlertParserTest extends TestCase ], [ "type" => "paragraph", - "text" => "From 11 PM this evening to 11 PM MST Thursday.", + "nodes" => [ + [ + "type" => "text", + "content" => + "From 11 PM this evening to 11 PM MST Thursday.", + ], + ], ], [ "type" => "heading", @@ -55,8 +72,14 @@ final class WeatherAlertParserTest extends TestCase ], [ "type" => "paragraph", - "text" => - "Travel could be very difficult. The hazardous conditions may impact travel over Wolf Creek Pass.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Travel could be very difficult. The hazardous conditions" . + " may impact travel over Wolf Creek Pass.", + ], + ], ], ]; @@ -82,8 +105,13 @@ final class WeatherAlertParserTest extends TestCase ], [ "type" => "paragraph", - "text" => - "Snow expected. Total snow accumulations of 5 to 10 inches.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Snow expected. Total snow accumulations of 5 to 10 inches.", + ], + ], ], ]; @@ -130,31 +158,46 @@ few power outages may result. * ADDITIONAL DETAILS...Snow levels will start near 6500 feet on Wednesday, then drop to near 5500 feet Wednesday night and near -5000 feet by Thursday morning. -"; +5000 feet by Thursday morning."; $expected = [ [ "type" => "paragraph", - "text" => - "WINTER CONDITIONS RETURN TO THE SIERRA AND NORTHEAST CALIFORNIA FOR MID-LATE WEEK", + "nodes" => [ + [ + "type" => "text", + "content" => + "WINTER CONDITIONS RETURN TO THE SIERRA" . + " AND NORTHEAST CALIFORNIA FOR MID-LATE WEEK", + ], + ], ], [ "type" => "paragraph", - "text" => - "This bit...has ellipses in the middle...but is not an overview.", + "nodes" => [ + [ + "type" => "text", + "content" => + "This bit...has ellipses in the middle...but is not an overview.", + ], + ], ], [ "type" => "paragraph", - "text" => - ".After a few days of warm weather, a potent winter storm" . - " will bring windy and colder conditions with periods of" . - " heavy snow to the Sierra and higher elevations of northeast" . - " California later this week. While weather-related travel impacts" . - " aren't expected through Wednesday morning, conditions will" . - " begin to worsen Wednesday afternoon and evening, with the most" . - " widespread winter travel impacts likely from Wednesday" . - " evening through much of Thursday.", + "nodes" => [ + [ + "type" => "text", + "content" => + ".After a few days of warm weather, a potent winter storm" . + " will bring windy and colder conditions with periods of" . + " heavy snow to the Sierra and higher elevations of northeast" . + " California later this week. While weather-related travel impacts" . + " aren't expected through Wednesday morning, conditions will" . + " begin to worsen Wednesday afternoon and evening, with the most" . + " widespread winter travel impacts likely from Wednesday" . + " evening through much of Thursday.", + ], + ], ], [ "type" => "heading", @@ -162,11 +205,16 @@ Wednesday, then drop to near 5500 feet Wednesday night and near ], [ "type" => "paragraph", - "text" => - "Heavy snow possible. Snow accumulations of 4 to 10 inches" . - " above 5000 feet west of US-395, with 10 to 20 inches possible" . - " for higher passes such as Fredonyer Summit and Yuba Pass." . - " Winds gusting as high as 50 mph.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Heavy snow possible. Snow accumulations of 4 to 10 inches" . + " above 5000 feet west of US-395, with 10 to 20 inches possible" . + " for higher passes such as Fredonyer Summit and Yuba Pass." . + " Winds gusting as high as 50 mph.", + ], + ], ], [ "type" => "heading", @@ -174,7 +222,13 @@ Wednesday, then drop to near 5500 feet Wednesday night and near ], [ "type" => "paragraph", - "text" => "Lassen-Eastern Plumas-Eastern Sierra Counties.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Lassen-Eastern Plumas-Eastern Sierra Counties.", + ], + ], ], [ "type" => "heading", @@ -182,9 +236,14 @@ Wednesday, then drop to near 5500 feet Wednesday night and near ], [ "type" => "paragraph", - "text" => - "From late Wednesday morning through Friday morning." . - " Heaviest snow is most likely from late Wednesday afternoon through Thursday morning.", + "nodes" => [ + [ + "type" => "text", + "content" => + "From late Wednesday morning through Friday morning." . + " Heaviest snow is most likely from late Wednesday afternoon through Thursday morning.", + ], + ], ], [ "type" => "heading", @@ -192,12 +251,17 @@ Wednesday, then drop to near 5500 feet Wednesday night and near ], [ "type" => "paragraph", - "text" => - "Travel could be very difficult at times," . - " with hazardous conditions impacting the commutes from" . - " Wednesday evening through Friday morning. Strong winds" . - " may blow down some tree limbs and a few power outages" . - " may result.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Travel could be very difficult at times," . + " with hazardous conditions impacting the commutes from" . + " Wednesday evening through Friday morning. Strong winds" . + " may blow down some tree limbs and a few power outages" . + " may result.", + ], + ], ], [ "type" => "heading", @@ -205,10 +269,15 @@ Wednesday, then drop to near 5500 feet Wednesday night and near ], [ "type" => "paragraph", - "text" => - "Snow levels will start near 6500 feet on Wednesday," . - " then drop to near 5500 feet Wednesday night and near" . - " 5000 feet by Thursday morning.", + "nodes" => [ + [ + "type" => "text", + "content" => + "Snow levels will start near 6500 feet on Wednesday," . + " then drop to near 5500 feet Wednesday night and near" . + " 5000 feet by Thursday morning.", + ], + ], ], ]; @@ -217,4 +286,235 @@ Wednesday, then drop to near 5500 feet Wednesday night and near $this->assertEquals($expected, $actual); } + + /** + * @group unit + * @group alert-parser + */ + public function testParsesBasicGovUrl() + { + $sourceText = "Scattered showers with brief heavy rainfall will continue this week. +A low pressure system from the north will batter your area, relentlessly. +Be sure to stock up. See https://www.weather.gov/safety/food for more information."; + $parser = new WeatherAlertParser(""); + $actual = $parser->extractURLs($sourceText); + $expected = [ + "type" => "link", + "url" => "https://www.weather.gov/safety/food", + "external" => false, + ]; + + $this->assertEquals(1, count($actual)); + $this->assertEquals($expected, $actual[0]); + } + + /** + * @group unit + * @group alert-parser + */ + public function testIgnoresNonGovUrls() + { + $sourceText = "Scattered showers with brief heavy rainfall will go on and +on and on, boring everyone who has to stay inside. They will attempt to visit sites +like https://fake.com/should/not/render which will not be clickable URLs."; + $parser = new WeatherAlertParser(""); + $actual = $parser->extractURLs($sourceText); + + $this->assertEquals(false, $actual); + } + + /** + * @group unit + * @group alert-parser + */ + public function testIgnoresUserPassUrls() + { + $sourceText = "Weather will, once again, be horrendous wherever you happen to be. +Maybe you are cursed? Anyway, the spammers would like you to click this link here that +has a username and password in the URL (https://root:1234@weather.gov/hack/the/gibson), which +you shouldn't be able to do thanks to Cautious Public Servants TM."; + $parser = new WeatherAlertParser(""); + $actual = $parser->extractURLs($sourceText); + + $this->assertEquals(false, $actual); + } + + /** + * @group unit + * @group alert-parser + */ + public function testAlertUrlNodesInternal() + { + $sourceText = + "* WHAT...There will be winds like you cannot believe." . + " See https://winds.weather.gov/info for more information."; + + $expected = [ + [ + "type" => "heading", + "text" => "what", + ], + [ + "type" => "paragraph", + "nodes" => [ + [ + "type" => "text", + "content" => + "There will be winds like you cannot believe. See ", + ], + [ + "type" => "link", + "url" => "https://winds.weather.gov/info", + "external" => false, + ], + [ + "type" => "text", + "content" => " for more information.", + ], + ], + ], + ]; + + $parser = new WeatherAlertParser($sourceText); + $actual = $parser->parse(); + + $this->assertEquals($expected, $actual); + } + + /** + * @group unit + * @group alert-parser + */ + public function testAlertUrlNodesExternal() + { + $sourceText = + "* IMPACTS...There will be shaking like you cannot believe." . + " See https://usgs.gov/earthquakes for more information."; + + $expected = [ + [ + "type" => "heading", + "text" => "impacts", + ], + [ + "type" => "paragraph", + "nodes" => [ + [ + "type" => "text", + "content" => + "There will be shaking like you cannot believe. See ", + ], + [ + "type" => "link", + "url" => "https://usgs.gov/earthquakes", + "external" => true, + ], + [ + "type" => "text", + "content" => " for more information.", + ], + ], + ], + ]; + + $parser = new WeatherAlertParser($sourceText); + $actual = $parser->parse(); + + $this->assertEquals($expected, $actual); + } + + /** + * @group unit + * @group alert-parser + */ + public function testAlertUrlNodesInvalid() + { + $sourceText = + "* WHEN...There will be weather like you cannot believe. " . + "See https://other-weather-site.com for more information, " . + "or even check out http://insecure.gov or else www.foo.net"; + + $expected = [ + [ + "type" => "heading", + "text" => "when", + ], + [ + "type" => "paragraph", + "nodes" => [ + [ + "type" => "text", + "content" => + "There will be weather like you cannot believe. " . + "See https://other-weather-site.com for more " . + "information, or even check " . + "out http://insecure.gov or else www.foo.net", + ], + ], + ], + ]; + + $parser = new WeatherAlertParser($sourceText); + $actual = $parser->parse(); + + $this->assertEquals($expected, $actual); + } + + /** + * @group unit + * @group alert-parser + * @group helpme + */ + public function testAlertUrlNodesDoubledRepresentation() + { + $sourceText = + "Damaging winds will blow down large objects such as trees" . + " and power lines. Power outages are expected. " . + "See www.your-power-company.com/outages for more information. " . + "Travel will be difficult, especially for high profile vehicles. " . + "For road safety, see https://transportation.gov/safe-travels . " . + "For more weather information, check out https://weather.gov/your-office " . + "for up to date forecasts and alerts."; + + $expected = [ + [ + "type" => "paragraph", + "nodes" => [ + [ + "type" => "text", + "content" => + "Damaging winds will blow down large objects such as trees" . + " and power lines. Power outages are expected. " . + "See www.your-power-company.com/outages for more information. " . + "Travel will be difficult, especially for high profile vehicles. " . + "For road safety, see ", + ], + [ + "type" => "link", + "url" => "https://transportation.gov/safe-travels", + "external" => true, + ], + [ + "type" => "text", + "content" => + " . For more weather information, check out ", + ], + [ + "type" => "link", + "url" => "https://weather.gov/your-office", + "external" => false, + ], + [ + "type" => "text", + "content" => " for up to date forecasts and alerts.", + ], + ], + ], + ]; + + $parser = new WeatherAlertParser($sourceText); + $actual = $parser->parse(); + + $this->assertEquals($expected, $actual); + } } diff --git a/web/modules/weather_data/src/Service/WeatherAlertParser.php b/web/modules/weather_data/src/Service/WeatherAlertParser.php index 11b131b95..2c15720a5 100644 --- a/web/modules/weather_data/src/Service/WeatherAlertParser.php +++ b/web/modules/weather_data/src/Service/WeatherAlertParser.php @@ -58,7 +58,7 @@ public function parse() if (!$parsedOverview && !$parsedWhatWhereWhen) { array_push($this->parsedNodes, [ "type" => "paragraph", - "text" => $paragraph, + "nodes" => $this->getParagraphNodesForString($paragraph), ]); } } @@ -84,7 +84,7 @@ public function parseOverview($str) if (preg_match($regex, $str, $matches)) { array_push($this->parsedNodes, [ "type" => "paragraph", - "text" => $matches[1], + "nodes" => $this->getParagraphNodesForString($matches[1]), ]); return true; @@ -116,7 +116,7 @@ public function parseWhatWhereWhen($str) ]); array_push($this->parsedNodes, [ "type" => "paragraph", - "text" => $matches["text"], + "nodes" => $this->getParagraphNodesForString($matches["text"]), ]); return true; @@ -124,4 +124,89 @@ public function parseWhatWhereWhen($str) return false; } + + /** + * Given a string that will be parsed into a paragraph + * node, determine if there are any valid links within it and, + * if so, responds with ordered text and link subnodes + */ + public function getParagraphNodesForString($str) + { + $links = $this->extractURLs($str); + if (!$links) { + return [ + [ + "type" => "text", + "content" => $str, + ], + ]; + } + + $nodes = []; + $current = $str; + foreach ($links as $link) { + $pos = strpos($current, $link["url"]); + $paraText = substr($current, 0, $pos); + array_push( + $nodes, + [ + "type" => "text", + "content" => $paraText, + ], + $link, + ); + $current = substr($current, $pos + strlen($link["url"])); + } + + if ($current && $current != "") { + array_push($nodes, [ + "type" => "text", + "content" => $current, + ]); + } + + return array_values($nodes); + } + + /** + * Attemps to parse out any valid URLs in the provided + * text body. + * Responds with a list of parsed objects if any are found, + * or false if none are found. + */ + public function extractURLs($str) + { + $regex = "/https\:\/\/[A-Za-z0-9\-._~:\/\?#\[\]@!$]+\b/"; + if (preg_match_all($regex, $str, $matches, PREG_OFFSET_CAPTURE)) { + $valid = array_filter($matches[0], function ($urlString) { + $url = parse_url($urlString[0]); + if (array_key_exists("user", $url)) { + return false; + } elseif (array_key_exists("pass", $url)) { + return false; + } elseif (!str_ends_with($url["host"], ".gov")) { + return false; + } + return true; + }); + + if (count($valid) == 0) { + return false; + } + + // Each link should be an assoc array + // with the URL along with data about + // whether it's internal or external + return array_map(function ($url) { + $parsedUrl = parse_url($url[0]); + $isInternal = str_contains($parsedUrl["host"], "weather.gov"); + return [ + "type" => "link", + "url" => $url[0], + "external" => !$isInternal, + ]; + }, array_values($valid)); + } + return false; + } } diff --git a/web/themes/new_weather_theme/templates/block/block--weathergov-local-alerts.html.twig b/web/themes/new_weather_theme/templates/block/block--weathergov-local-alerts.html.twig index 88cf93079..081dd3ecf 100644 --- a/web/themes/new_weather_theme/templates/block/block--weathergov-local-alerts.html.twig +++ b/web/themes/new_weather_theme/templates/block/block--weathergov-local-alerts.html.twig @@ -33,8 +33,20 @@ {% for element in alert.description %} {% if element.type == "heading" %}

{{ element.text | capitalize }}

+ {% elseif element.type == "paragraph" %} +

+ {% for node in element.nodes %} + {% if node.type == "link" %} + + {{node.url}} + + {% else %} + {{ node.content }} + {% endif %} + {% endfor %} +

{% else %} -

{{ element.text }}

+ {{ node }} {% endif %} {% endfor %}