test(visual-regression): Add percy tests and improve coverage (#4065)

This commit is contained in:
Ibrahim 2024-05-14 13:27:21 -04:00 committed by GitHub
parent 953d9eed9c
commit 997a1afd7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 261 additions and 432 deletions

View File

@ -1 +0,0 @@
PERCY_TOKEN=<your token here>

View File

@ -154,6 +154,7 @@ export default function PanelPetSUV({ servicesManager, commandsManager }: withAp
PatientWeight: e.target.value,
});
}}
id="weight-input"
/>
<Input
containerClassName={'!flex-row !justify-between items-center'}

View File

@ -25,6 +25,7 @@ describe('OHIF HP', () => {
});
it('Should navigate to display set specified', () => {
Cypress.on('uncaught:exception', () => false);
// This filters by series instance UID, meaning there will only be 1 thumbnail
// It applies the initial SOP instance, navigating to that image
cy.checkStudyRouteInViewer(

View File

@ -1,34 +0,0 @@
/*describe('Visual Regression - OHIF Cornerstone Hotkeys', () => {
before(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if hotkey "I" can invert the image', () => {
// Hotkey I
cy.get('body').type('I');
// Visual comparison
cy.screenshot('Hotkey I - Should Invert Image');
cy.percyCanvasSnapshot('Hotkey I - Should Invert Image');
});
it('checks if hotkey "SPACEBAR" can reset the image', () => {
// Press multiples hotkeys
cy.get('body').type('V+++I');
// Hotkey SPACEBAR
cy.get('body').type(' ');
// Visual comparison to make sure the 'inverted' image was reset
cy.screenshot('Hotkey SPACEBAR - Should Reset Image');
cy.percyCanvasSnapshot('Hotkey SPACEBAR - Should Reset Image');
});
});*/

View File

@ -1,85 +0,0 @@
/*describe('Visual Regression - OHIF Cornerstone Toolbar', () => {
before(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if Pan tool will move the image inside the viewport', () => {
//Click on button and verify if icon is active on toolbar
cy.get('@panBtn')
.click()
.then($panBtn => {
cy.wrap($panBtn).should('have.class', 'active');
});
cy.get('@viewport')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'bottom', { which: 1 })
.trigger('mouseup', 'bottom');
// Visual comparison
cy.percyCanvasSnapshot('Pan tool moved the image inside the viewport');
});
it('check if Invert tool will change the colors of the image in the viewport', () => {
// Click on More button
cy.get('@moreBtn').click();
// Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
// Click on Invert button
cy.get('[data-cy="invert"]').click();
// Visual comparison
cy.percyCanvasSnapshot('Invert tool - Should Invert Canvas');
});
it('check if Rotate tool will change the image orientation in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay')
.should('be.visible')
.then(() => {
//Click on Rotate button
cy.get('[data-cy="rotate right"]').click({ force: true });
});
// Visual comparison
cy.percyCanvasSnapshot('Rotate tool - Should Rotate Image to Right');
});
it('check if Flip H tool will flip the image horizontally in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip H button
cy.get('[data-cy="flip h"]').click();
// Visual comparison
cy.percyCanvasSnapshot('Flip H tool - Should Flip Image on Y axis');
});
it('check if Flip V tool will flip the image vertically in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button
cy.get('[data-cy="flip v"]').click();
// Visual comparison
cy.percyCanvasSnapshot('Flip V tool - Should Flip Image on X axis');
});
});*/

View File

@ -1,44 +0,0 @@
/*describe('Visual Regression - OHIF Download Snapshot File', () => {
before(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.openDownloadImageModal();
});
afterEach(() => {
// Close modal
cy.get('[data-cy="close-button"]')
.scrollIntoView()
.click();
});
it('checks displayed information for Desktop experience', function() {
// Set Desktop resolution
cy.viewport(1750, 720);
// Visual comparison
cy.percyCanvasSnapshot('Download Image Modal - Desktop experience');
});
it('checks if "Show Annotations" checkbox will display annotations', function() {
// Close modal that is initially opened
cy.get('[data-cy="close-button"]').click();
// Add measurements in the viewport
cy.addLengthMeasurement();
cy.addAngleMeasurement();
// Open Modal
cy.openDownloadImageModal();
// Select "Show Annotations" option
cy.get('[data-cy="show-annotations"]').check();
// Check image preview
cy.get('[data-cy="image-preview"]').scrollIntoView();
// Visual comparison
cy.percyCanvasSnapshot('Download Image Modal - Show Annotations checked');
});
});*/

View File

@ -1,36 +0,0 @@
/*describe('Visual Regression - OHIF Study Viewer Page', function() {
before(() => {
cy.checkStudyRouteInViewer(
'1.2.840.113619.2.5.1762583153.215519.978957063.78'
);
cy.expectMinimumThumbnails(3);
});
beforeEach(function() {
cy.initCommonElementsAliases();
cy.resetViewport();
});
it('checks if series thumbnails are being displayed', function() {
cy.percyCanvasSnapshot('Series Thumbnails - Should Display Thumbnails');
});
it('opens About modal and verify the displayed information', function() {
cy.get('[data-cy="options-menu"]')
.first()
.click();
cy.get('[data-cy="dd-item-menu"]')
.first()
.click();
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
// Visual comparison
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.exist');
});
});*/

View File

@ -1,204 +0,0 @@
/*describe('Visual Regression - OHIF User Preferences', () => {
context('Study List Page', function() {
before(() => {
cy.visit('/');
});
beforeEach(() => {
// Open User Preferences modal
cy.openPreferences();
});
afterEach(() => {
// Close User Preferences modal
cy.closePreferences();
});
it('checks displayed information on User Preferences modal', function() {
// Go go hotkeys tab
cy.selectPreferencesTab('@userPreferencesHotkeysTab');
cy.get('@restoreBtn').scrollIntoView();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in StudyList page'
);
cy.get('[data-cy="close-button"]').click();
});
it('checks translation by selecting Spanish language', function() {
cy.selectPreferencesTab('@userPreferencesGeneralTab');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in StudyList page'
);
// Set language to Spanish and save
cy.setLanguage('Spanish');
// Options menu should be translated
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Opciones')
.click();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Spanish selected in StudyList page'
);
// Close Options menu
cy.get('[data-cy="options-menu"]').click();
});
});
context('Study Viewer Page', function() {
before(() => {
cy.openStudyInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');
cy.expectMinimumThumbnails(3);
});
beforeEach(() => {
cy.initCommonElementsAliases();
cy.resetViewport();
cy.resetUserHotkeyPreferences();
// Open User Preferences modal
cy.openPreferences();
});
afterEach(() => {
// Close User Preferences modal
cy.closePreferences();
});
it('checks displayed information on User Preferences modal', function() {
// Go go hotkeys tab
cy.selectPreferencesTab('@userPreferencesHotkeysTab');
cy.get('@restoreBtn').scrollIntoView();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in StudyViewer page'
);
cy.get('[data-cy="close-button"]').click(); //close User Preferences modal
});
it('checks if W/L Preferences table is being displayed in the Window Level tab', function() {
//Navigate to Window Level tab
cy.selectPreferencesTab('@userPreferencesWindowLevelTab');
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Window Level Presets Tab'
);
});
it('checks translation by selecting Spanish language', function() {
cy.selectPreferencesTab('@userPreferencesGeneralTab');
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in StudyViewer page'
);
// Set language to Spanish
cy.setLanguage('Spanish');
// Options menu should be translated
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Opciones')
.click();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - Spanish selected in StudyViewer page'
);
// Close Options menu
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can restore to default the language selection and application will be in English', function() {
cy.selectPreferencesTab('@userPreferencesGeneralTab');
// Set language to Spanish
cy.setLanguage('Spanish');
// Open User Preferences modal
cy.openPreferences();
// Go to general tab
cy.selectPreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
.click();
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Options menu should be in English
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
// Visual comparison
cy.percyCanvasSnapshot(
'User Preferences Modal - English selected in StudyViewer page'
);
// Close Options menu
cy.get('[data-cy="options-menu"]').click();
});
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.selectPreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Next Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Next Viewport',
'{shift}{rightarrow}'
);
// Set new hotkey for 'Previous Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Previous Viewport',
'{shift}{leftarrow}'
);
// Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set 3 viewports layout
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Rotate Right and Invert colors on Viewport #1
cy.get('body').type('RI');
//Move to Next Viewport
cy.get('body').type('{shift}{rightarrow}');
// Rotate Left and Invert colors on Viewport #2
cy.get('body').type('LI');
//Move to Previous Viewport
cy.get('body').type('{shift}{leftarrow}');
// Reset viewport #1 with spacebar hotkey
cy.get('body').type(' ');
// Visual comparison
cy.percyCanvasSnapshot(
'Viewport Navigation - 2nd viewport inverted and rotated'
);
// Set 1 viewport layout
cy.setLayout(1, 1);
});
});
});
*/

View File

@ -3,6 +3,7 @@
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
beforeEach(function () {
Cypress.on('uncaught:exception', () => false);
cy.window().then(win => win.sessionStorage.clear());
cy.openStudyList();

View File

@ -0,0 +1,25 @@
import '@percy/cypress';
describe('OHIF Percy Layout Advanced Presets', () => {
beforeEach(() => {
cy.openStudyInViewer('1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1');
cy.wait(5000);
cy.expectMinimumThumbnails(3);
});
it('should display each layout preset correctly', () => {
cy.get('[data-cy="study-browser-thumbnail"]').eq(2).dblclick();
cy.selectLayoutPreset('MPR', true);
cy.percyCanvasSnapshot('MPR Preset');
cy.selectLayoutPreset('3D four up');
cy.percyCanvasSnapshot('3D four up Preset');
cy.selectLayoutPreset('3D main');
cy.percyCanvasSnapshot('3D main Preset');
cy.selectLayoutPreset('Axial Primary');
cy.percyCanvasSnapshot('Axial Primary Preset');
cy.selectLayoutPreset('3D only');
cy.percyCanvasSnapshot('3D only Preset');
cy.selectLayoutPreset('3D primary');
cy.percyCanvasSnapshot('3D primary Preset');
});
});

View File

@ -0,0 +1,31 @@
import '@percy/cypress';
describe('OHIF Percy MPR Crosshairs', () => {
beforeEach(() => {
cy.openStudyInViewer(
'1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463',
'&hangingProtocolId=default',
'/viewer'
);
cy.wait(5000);
cy.expectMinimumThumbnails(4);
});
it('should enabled crosshairs in MPR and reset them when another series is loaded', () => {
cy.get('[data-cy="study-browser-thumbnail"]').eq(2).dblclick();
cy.selectLayoutPreset('MPR');
cy.get('[data-cy="Crosshairs"]').click();
cy.wait(500);
cy.percyCanvasSnapshot('Crosshairs enabled');
cy.wait(100);
cy.get('[data-viewport-uid="mpr-axial"] > .viewport-element > .cornerstone-canvas').click(
45,
100
);
cy.wait(200);
cy.percyCanvasSnapshot('Crosshairs are moved');
cy.get('[data-cy="study-browser-thumbnail"]').eq(0).dblclick();
cy.wait(500);
cy.percyCanvasSnapshot('Another series dropped, Crosshairs are centered');
});
});

View File

@ -0,0 +1,13 @@
import '@percy/cypress';
describe('OHIF Percy PDF Display', () => {
beforeEach(() => {
cy.openStudyInViewer('1.2.826.0.13854362241694438965858641723883466450351448');
cy.wait(5000);
cy.expectMinimumThumbnails(1);
});
it('should display PDFs correctly', () => {
cy.percyCanvasSnapshot('PDF');
});
});

View File

@ -0,0 +1,26 @@
import '@percy/cypress';
describe('OHIF Percy SR', () => {
beforeEach(() => {
cy.openStudyInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');
cy.wait(5000);
cy.expectMinimumThumbnails(3);
});
it('should hydrate SR correctly and Jump to Measurements', () => {
cy.get('[data-cy="study-browser-thumbnail-no-image"]').first().dblclick();
cy.wait(200);
cy.percyCanvasSnapshot('SR Preview');
cy.get('body').type('{enter}');
cy.wait(200);
cy.percyCanvasSnapshot('SR Hydrated');
cy.get('[data-cy="trackedMeasurements-btn"]').click();
cy.percyCanvasSnapshot('Opened Measurement Panel');
cy.get('body').type('{downarrow}');
cy.get('body').type('{downarrow}');
cy.get('body').type('{downarrow}');
cy.percyCanvasSnapshot('Scrolled away from measurements');
cy.get('[data-cy="measurement-item"]').first().click();
cy.percyCanvasSnapshot('Jumped to Measurement');
});
});

View File

@ -0,0 +1,42 @@
import '@percy/cypress';
describe('OHIF Percy Segmentation Tools', () => {
beforeEach(() => {
Cypress.on('uncaught:exception', () => false);
cy.openStudyInViewer(
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
'&hangingProtocolId=default',
'/segmentation'
);
cy.wait(5000);
cy.expectMinimumThumbnails(3);
});
it('should be able to use all segmentation tools and have them render correctly', () => {
cy.get('[data-cy="study-browser-thumbnail"]').eq(1).dblclick();
cy.percyCanvasSnapshot('Segmentation tools are disabled');
cy.get('span').contains('Add segmentation').should('have.css', 'pointer-events', 'none');
cy.percyCanvasSnapshot(
'Add segmentation button is disabled when the displayset is none reconstructable'
);
cy.get('[data-cy="study-browser-thumbnail"]').eq(2).dblclick();
cy.get('span').contains('Add segmentation').click();
cy.wait(500);
cy.percyCanvasSnapshot('Segmentation added and tools are enabled');
cy.get('[data-cy="Brush"]').click();
cy.percyCanvasSnapshot('Brush tool selected');
cy.addBrush('.cornerstone-canvas');
cy.percyCanvasSnapshot('Brush tool applied');
cy.get('[data-cy="Eraser"]').click();
cy.percyCanvasSnapshot('Eraser tool selected');
cy.addEraser('.cornerstone-canvas');
cy.percyCanvasSnapshot('Eraser tool applied');
cy.selectLayoutPreset('MPR');
cy.get('[data-cy="Brush"]').click();
cy.percyCanvasSnapshot('Brush tool selected in MPR');
cy.get('button').contains('Sphere').click();
cy.percyCanvasSnapshot('Sphere mode for Brush tool selected');
cy.addBrush('[data-viewport-uid="mpr-axial"] > .viewport-element > .cornerstone-canvas');
cy.percyCanvasSnapshot('Sphere stroke for Brush tool applied');
});
});

View File

@ -0,0 +1,38 @@
import '@percy/cypress';
describe('OHIF Percy TMTV', () => {
beforeEach(() => {
cy.openStudyInViewer(
'1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463',
'',
'/tmtv'
);
cy.wait(10000);
});
it('should maintain VOI when going 1 up and back', () => {
cy.percyCanvasSnapshot('TMTV Loaded');
cy.get('[data-viewport-uid="mipSagittal"] > .viewport-element > .cornerstone-canvas').dblclick(
100,
100
);
cy.wait(500);
cy.percyCanvasSnapshot('TMTV One up view loaded');
cy.get('[data-viewport-uid="mipSagittal"] > .viewport-element > .cornerstone-canvas').dblclick(
100,
100
);
cy.wait(500);
cy.percyCanvasSnapshot('TMTV Back to original view');
});
it('should still look fine when changing patient data', () => {
cy.get('[data-cy="petSUV-btn"]').click();
cy.percyCanvasSnapshot('TMTV Patient Panel Opened');
cy.get('[data-cy="input-weight-input"]').type('191');
cy.percyCanvasSnapshot('TMTV Patient Panel Weight Changed');
cy.get('button').contains('Reload Data').click();
cy.wait(5000);
cy.percyCanvasSnapshot('TMTV Reloaded Data');
});
});

View File

@ -0,0 +1,13 @@
import '@percy/cypress';
describe('OHIF Percy Video Display', () => {
beforeEach(() => {
cy.openStudyInViewer('2.25.96975534054447904995905761963464388233');
cy.wait(5000);
cy.expectMinimumThumbnails(0);
});
it('should display Videos correctly', () => {
cy.percyCanvasSnapshot('Video');
});
});

View File

@ -0,0 +1 @@
test-output.xml

View File

@ -6,36 +6,26 @@ import {
initCommonElementsAliases,
initRouteAliases,
initStudyListAliasesOnDesktop,
initStudyListAliasesOnTablet,
initPreferencesModalAliases,
initPreferencesModalFooterBtnAliases,
} from './aliases.js';
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
/**
* Command to select a layout preset.
* The layout preset is selected by clicking on the Layout button and then clicking on the desired preset.
* The preset name is the text that is displayed on the button.
* @param {string} presetName - The name of the layout preset that we would like to select
* @param {boolean} screenshot - If true, a screenshot will be taken when the layout tool is opened
*/
Cypress.Commands.add('selectLayoutPreset', (presetName, screenshot) => {
cy.get('[data-cy="Layout"]').click();
if (screenshot) {
cy.percyCanvasSnapshot('Layout tool opened');
}
cy.get('div').contains(presetName).should('be.visible').click();
// fixed wait time for layout changes and rendering
cy.wait(3000);
});
/**
* Command to search for a patient name and open his/her study.
@ -288,6 +278,56 @@ Cypress.Commands.add(
}
);
// Add brush stroke in the viewport
Cypress.Commands.add('addBrush', (viewport, firstClick = [85, 100], secondClick = [85, 300]) => {
cy.get(viewport)
.first()
.then(viewportElement => {
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
const steps = 10;
const xStep = (x2 - x1) / steps;
const yStep = (y2 - y1) / steps;
cy.wrap(viewportElement)
.trigger('mousedown', x1, y1, { buttons: 1 })
.then(() => {
for (let i = 1; i <= steps; i++) {
let x = x1 + xStep * i;
let y = y1 + yStep * i;
cy.wrap(viewportElement).trigger('mousemove', x, y, { buttons: 1 });
}
})
.trigger('mouseup');
});
});
// Add erase stroke in the viewport
Cypress.Commands.add('addEraser', (viewport, firstClick = [85, 100], secondClick = [85, 300]) => {
cy.get(viewport)
.first()
.then(viewportElement => {
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
const steps = 10;
const xStep = (x2 - x1) / steps;
const yStep = (y2 - y1) / steps;
cy.wrap(viewportElement)
.trigger('mousedown', x1, y1, { buttons: 1 })
.then(() => {
for (let i = 1; i <= steps; i++) {
let x = x1 + xStep * i;
let y = y1 + yStep * i;
cy.wrap(viewportElement).trigger('mousemove', x, y, { buttons: 1 });
}
})
.trigger('mouseup');
});
});
//Add measurements in the viewport
Cypress.Commands.add(
'addAngleMeasurement',

View File

@ -36,7 +36,7 @@
"dev:viewer": "yarn run dev",
"start": "yarn run dev",
"test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
"test:e2e:ci": "percy exec -- cypress run --config video=false --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
"test:e2e:local": "cypress run --config video=false --browser chrome --spec 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'",
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
"test:e2e:serve": "cross-env APP_CONFIG=config/e2e.js yarn start",

@ -1 +1 @@
Subproject commit c9892af83d5dc4b06fac6ff1ca77246467571ec0
Subproject commit bd743ed76b403e6d476e1af0bbfe7479d96b563d

View File

@ -1575,6 +1575,7 @@
pako "^2.0.4"
uuid "^9.0.0"
"@cornerstonejs/streaming-image-volume-loader@^1.71.2":
version "1.71.2"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.71.2.tgz#64b92c71f1550000edaa79f10259ebec05092748"