Quick View: Add search results snippet to QuickView panel
- [x] Add snippets to QuickView - [x] Add "Go to full article" link - [x] Restore QuickView on Back button - [x] Set Jest Mocks and configuration - [x] Update unit tests Bug: T311644 Change-Id: I2de94e97f6d2800f66b4fda0554e104aa16fcd75
这个提交包含在:
父节点
19dc8f69ec
当前提交
5f308b27a7
|
@ -1,3 +1,4 @@
|
|||
/coverage
|
||||
/composer.lock
|
||||
/vendor
|
||||
/node_modules
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"resources/components/App.vue",
|
||||
"resources/components/QuickView.vue",
|
||||
"resources/components/LeadImage.vue",
|
||||
"resources/components/LeadText.vue",
|
||||
"resources/components/QuickViewSnippet.vue",
|
||||
"resources/store/index.js",
|
||||
"resources/store/state.js",
|
||||
"resources/store/mutations.js",
|
||||
|
@ -72,7 +72,8 @@
|
|||
"messages": [
|
||||
"quickview-close",
|
||||
"quickview-previous",
|
||||
"quickview-next"
|
||||
"quickview-next",
|
||||
"quickview-snippet-gotofullarticle"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"quickview-close": "Close",
|
||||
"quickview-next": "Next",
|
||||
"quickview-previous": "previous",
|
||||
"quickview-snippet-gotofullarticle": "Go to full article",
|
||||
"searchpreview-label": "Enable search result preview",
|
||||
"searchpreview-help": "Preview the contents of the article and related pages."
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"quickview-close": "Title for the svg used to close the Quick View on mobile view",
|
||||
"quickview-next": "Title of the svg used to move to the next result",
|
||||
"quickview-previous": "Title of the svg used to move to the previous result",
|
||||
"quickview-snippet-gotofullarticle": "Button shown at the bottom of the result snippet to navigate to the full article",
|
||||
"searchpreview-label": "Used in [[Special:Preferences]], tab \"Search\". Offer the user the ability to enable or disable the search preview feature.",
|
||||
"searchpreview-help": "Help text for user preference that offer the user the ability to enable or disable the search preview feature."
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/lw/zjjyns4n3n11bnzbgvqkh1_m0000gn/T/jest_dx",
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
collectCoverageFrom: [
|
||||
'resources/**/*.(js|vue)'
|
||||
],
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
coveragePathIgnorePatterns: [
|
||||
'/node_modules/'
|
||||
],
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
setupFiles: [
|
||||
'./jest.setup.js'
|
||||
],
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom',
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/'
|
||||
],
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest'
|
||||
}
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
const $ = require( 'jquery' );
|
||||
|
||||
/* global jest:false */
|
||||
function Api() {}
|
||||
Api.prototype.get = jest.fn().mockReturnValue( $.Deferred().resolve().promise() );
|
||||
Api.prototype.post = jest.fn().mockResolvedValue( {} );
|
||||
Api.prototype.getToken = jest.fn().mockResolvedValue( {} );
|
||||
Api.prototype.postWithToken = jest.fn().mockResolvedValue( {} );
|
||||
Api.prototype.saveOption = jest.fn();
|
||||
|
||||
function Title() {}
|
||||
Title.prototype.getMainText = jest.fn().mockReturnValue( '' );
|
||||
Title.prototype.getName = jest.fn().mockReturnValue( '' );
|
||||
Title.prototype.getNamespaceId = jest.fn().mockReturnValue( 0 );
|
||||
Title.prototype.getNamespacePrefix = jest.fn().mockReturnValue( '' );
|
||||
Title.newFromText = jest.fn().mockReturnValue( {
|
||||
fragment: null,
|
||||
namespace: 0,
|
||||
title: '',
|
||||
getMainText: jest.fn().mockReturnValue( 'mock' ),
|
||||
getName: jest.fn().mockReturnValue( 'mock' ),
|
||||
getExtension: jest.fn().mockReturnValue( 'mock' ),
|
||||
getNamespaceId: jest.fn().mockReturnValue( 0 )
|
||||
} );
|
||||
|
||||
const mw = {
|
||||
Api: Api,
|
||||
config: {
|
||||
get: jest.fn()
|
||||
},
|
||||
message: jest.fn().mockReturnValue( {
|
||||
text: jest.fn(),
|
||||
parse: jest.fn(),
|
||||
params: jest.fn( () => mw.message() )
|
||||
} ),
|
||||
msg: jest.fn().mockReturnValue( '' ),
|
||||
Uri: jest.fn().mockReturnValue( {
|
||||
getQueryString: jest.fn(),
|
||||
query: {
|
||||
type: ''
|
||||
}
|
||||
} ),
|
||||
Title: Title,
|
||||
util: {
|
||||
parseImageUrl: jest.fn().mockReturnValue( {
|
||||
resizeUrl: jest.fn()
|
||||
} )
|
||||
},
|
||||
language: {
|
||||
convertNumber: jest.fn()
|
||||
},
|
||||
storage: {
|
||||
get: jest.fn(),
|
||||
setObject: jest.fn(),
|
||||
getObject: jest.fn(),
|
||||
remove: jest.fn()
|
||||
},
|
||||
notify: jest.fn(),
|
||||
OgvJsSupport: {
|
||||
basePath: jest.fn(),
|
||||
loadIfNeeded: jest.fn()
|
||||
},
|
||||
loader: {
|
||||
using: jest.fn()
|
||||
},
|
||||
user: {
|
||||
options: {
|
||||
get: jest.fn().mockReturnValue( 0 ),
|
||||
set: jest.fn()
|
||||
},
|
||||
isAnon: jest.fn().mockReturnValue( false )
|
||||
}
|
||||
};
|
||||
/*
|
||||
* MW front-end code expects certain global variables to exist in the
|
||||
* environment. This sets up things like "mw" object, jquery, etc. for use
|
||||
* in tests.
|
||||
*/
|
||||
global.mw = mw;
|
||||
global.$ = require( 'jquery' );
|
|
@ -64,6 +64,16 @@ module.exports = exports = {
|
|||
// Set the correct offset to align with the search results
|
||||
this.offsetTop = element.offsetTop || 0 + 'px';
|
||||
}
|
||||
},
|
||||
restoreQuickViewOnNavigation() {
|
||||
const mwUri = new mw.Uri();
|
||||
|
||||
const queryHasQuickView = !!mwUri.query.quickView;
|
||||
|
||||
if ( queryHasQuickView ) {
|
||||
const title = mwUri.query.quickView;
|
||||
this.handleTitleChange( title );
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -96,6 +106,10 @@ module.exports = exports = {
|
|||
this.closeQuickView();
|
||||
}
|
||||
}.bind( this ) );
|
||||
|
||||
// Restore the quick view in the case in which the user has navigated back to a page
|
||||
// that had a quickView open
|
||||
this.restoreQuickViewOnNavigation();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -109,9 +123,8 @@ module.exports = exports = {
|
|||
&__desktop {
|
||||
border: solid 1px #c8CCd1;
|
||||
right: 0px;
|
||||
max-width: 30em;
|
||||
width: 30em;
|
||||
display: inline-block;
|
||||
margin-left: 8%;
|
||||
}
|
||||
|
||||
&__mobile {
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<p>{{ title }}</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @file LeadText.vue
|
||||
*
|
||||
* Placeholder
|
||||
*/
|
||||
const mapState = require( 'vuex' ).mapState;
|
||||
|
||||
// @vue/component
|
||||
module.exports = exports = {
|
||||
name: 'LeadImage',
|
||||
computed: $.extend( {},
|
||||
mapState( [
|
||||
'title'
|
||||
] )
|
||||
)
|
||||
};
|
||||
</script>
|
|
@ -30,7 +30,10 @@
|
|||
<header>
|
||||
<lead-image></lead-image>
|
||||
</header>
|
||||
<lead-text></lead-text>
|
||||
<quick-view-snippet
|
||||
:text="currentResult.text"
|
||||
:title="currentResult.prefixedText"
|
||||
></quick-view-snippet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -41,8 +44,9 @@
|
|||
* Placeholder
|
||||
*/
|
||||
const LeadImage = require( './LeadImage.vue' ),
|
||||
LeadText = require( './LeadText.vue' ),
|
||||
QuickViewSnippet = require( './QuickViewSnippet.vue' ),
|
||||
mapActions = require( 'vuex' ).mapActions,
|
||||
mapGetters = require( 'vuex' ).mapGetters,
|
||||
mapState = require( 'vuex' ).mapState;
|
||||
|
||||
// @vue/component
|
||||
|
@ -50,7 +54,7 @@ module.exports = exports = {
|
|||
name: 'QuickView',
|
||||
components: {
|
||||
'lead-image': LeadImage,
|
||||
'lead-text': LeadText
|
||||
'quick-view-snippet': QuickViewSnippet
|
||||
},
|
||||
data: function () {
|
||||
return {};
|
||||
|
@ -58,6 +62,9 @@ module.exports = exports = {
|
|||
computed: $.extend( {},
|
||||
mapState( [
|
||||
'isMobile'
|
||||
] ),
|
||||
mapGetters( [
|
||||
'currentResult'
|
||||
] )
|
||||
),
|
||||
methods: $.extend( {},
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="quickViewSnippet">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p v-html="text"></p>
|
||||
<a :href="url">{{ $i18n( 'quickview-snippet-gotofullarticle' ).text() }}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @file LeadText.vue
|
||||
*
|
||||
* Placeholder
|
||||
*/
|
||||
|
||||
// @vue/component
|
||||
module.exports = exports = {
|
||||
name: 'QuickViewSnippet',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
url() {
|
||||
const title = new mw.Title( this.title );
|
||||
return title.getUrl();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.quickViewSnippet{
|
||||
width: 365px;
|
||||
margin: 4px auto;
|
||||
}
|
||||
</style>
|
|
@ -1,4 +1,39 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Push the current provided title to the browser's session history stack
|
||||
*
|
||||
* @param {string} title
|
||||
*/
|
||||
const pushTitleToHistoryState = ( title ) => {
|
||||
const mwUri = new mw.Uri();
|
||||
// update mw URI query object with the one currently available within the store
|
||||
// In Vue 3, context.state.uriQuery is a Proxy, and passing it to replaceState()
|
||||
// causes an error saying it can't be cloned. Work around this by cloning the uriQuery
|
||||
// object ourselves, using JSON.parse( JSON.stringify() ) to convert the Proxy to Object.
|
||||
const existingQuery = JSON.parse( JSON.stringify( mwUri.query ) );
|
||||
mwUri.query = $.extend(
|
||||
{},
|
||||
existingQuery,
|
||||
{ quickView: title }
|
||||
);
|
||||
const queryString = '?' + mwUri.getQueryString();
|
||||
window.history.pushState( mwUri.query, null, queryString );
|
||||
};
|
||||
/**
|
||||
* Remove the value of QuickView from the history State.
|
||||
*/
|
||||
const removeQuickViewFromHistoryState = () => {
|
||||
const mwUri = new mw.Uri();
|
||||
// update mw URI query object with the one currently available within the store
|
||||
// In Vue 3, context.state.uriQuery is a Proxy, and passing it to replaceState()
|
||||
// causes an error saying it can't be cloned. Work around this by cloning the uriQuery
|
||||
// object ourselves, using JSON.parse( JSON.stringify() ) to convert the Proxy to Object.
|
||||
mwUri.query = JSON.parse( JSON.stringify( mwUri.query ) );
|
||||
delete mwUri.query.quickView;
|
||||
const queryString = '?' + mwUri.getQueryString();
|
||||
window.history.pushState( mwUri.query, null, queryString );
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
|
@ -15,13 +50,13 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
// Close the quickview if the snippets clicked is current one
|
||||
// Close the quickview if the snippet clicked is current one
|
||||
if ( context.state.title === title ) {
|
||||
context.commit( 'SET_TITLE', null );
|
||||
context.commit( 'SET_SELECTED_INDEX', -1 );
|
||||
context.dispatch( 'closeQuickView' );
|
||||
} else {
|
||||
// TODO -> Retrieve snippets information and save them in the store
|
||||
// TODO -> Retrieve snippet information and save them in the store
|
||||
context.commit( 'SET_TITLE', title );
|
||||
pushTitleToHistoryState( title );
|
||||
|
||||
const selectedTitleIndex = context.state.results.findIndex( ( result ) => {
|
||||
return result.prefixedText === title;
|
||||
|
@ -38,6 +73,8 @@ module.exports = {
|
|||
*/
|
||||
closeQuickView: ( context ) => {
|
||||
context.commit( 'SET_TITLE', null );
|
||||
context.commit( 'SET_SELECTED_INDEX', -1 );
|
||||
removeQuickViewFromHistoryState();
|
||||
},
|
||||
/**
|
||||
* Navigate results
|
||||
|
|
|
@ -10,5 +10,21 @@ module.exports = {
|
|||
*/
|
||||
visible: ( state ) => {
|
||||
return state.title !== null;
|
||||
},
|
||||
/**
|
||||
* Returns the currently selected result.
|
||||
*
|
||||
* @param {Object} state
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
currentResult: ( state ) => {
|
||||
|
||||
if ( state.selectedIndex === -1 ) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return state.results[ state.selectedIndex ];
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
window.history.pushState = jest.fn();
|
||||
window.history.replaceState = jest.fn();
|
|
@ -1,5 +1,7 @@
|
|||
const initialState = require( '../fixtures/initialVuexState.js' );
|
||||
|
||||
require( '../mocks/history.js' );
|
||||
|
||||
let context;
|
||||
let actions;
|
||||
|
||||
|
@ -19,6 +21,7 @@ beforeEach( () => {
|
|||
actions.getters = context.getters;
|
||||
actions.state = context.state;
|
||||
actions.commit = context.commit;
|
||||
actions.dispatch = context.dispatch;
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
|
@ -38,22 +41,13 @@ describe( 'Actions', () => {
|
|||
} );
|
||||
} );
|
||||
describe( 'when title provided is the same as state.title', () => {
|
||||
it( 'Set title to null', () => {
|
||||
it( 'Dispacth a call to closeQuickView', () => {
|
||||
const title = 'dummy';
|
||||
context.state.title = title;
|
||||
actions.handleTitleChange( context, title );
|
||||
|
||||
expect( actions.commit ).toHaveBeenCalled();
|
||||
expect( actions.commit ).toHaveBeenCalledWith( 'SET_TITLE', null );
|
||||
|
||||
} );
|
||||
it( 'Set selected index to -1', () => {
|
||||
const title = 'dummy';
|
||||
context.state.title = title;
|
||||
actions.handleTitleChange( context, title );
|
||||
|
||||
expect( actions.commit ).toHaveBeenCalled();
|
||||
expect( actions.commit ).toHaveBeenCalledWith( 'SET_SELECTED_INDEX', -1 );
|
||||
expect( actions.dispatch ).toHaveBeenCalled();
|
||||
expect( actions.dispatch ).toHaveBeenCalledWith( 'closeQuickView' );
|
||||
|
||||
} );
|
||||
} );
|
||||
|
@ -86,6 +80,20 @@ describe( 'Actions', () => {
|
|||
expect( actions.commit ).toHaveBeenCalledWith( 'SET_SELECTED_INDEX', 1 );
|
||||
|
||||
} );
|
||||
|
||||
it( 'Adds QuickView to history state', () => {
|
||||
const title = 'dummy';
|
||||
actions.handleTitleChange( context, title );
|
||||
|
||||
expect( window.history.pushState ).toHaveBeenCalled();
|
||||
expect( window.history.pushState ).toHaveBeenCalledWith(
|
||||
expect.objectContaining(
|
||||
{ quickView: title }
|
||||
),
|
||||
null,
|
||||
expect.anything()
|
||||
);
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
@ -182,4 +190,40 @@ describe( 'Actions', () => {
|
|||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'closeQuickView', () => {
|
||||
it( 'Set title to null', () => {
|
||||
const title = 'dummy';
|
||||
context.state.title = title;
|
||||
actions.closeQuickView( context, title );
|
||||
|
||||
expect( actions.commit ).toHaveBeenCalled();
|
||||
expect( actions.commit ).toHaveBeenCalledWith( 'SET_TITLE', null );
|
||||
|
||||
} );
|
||||
it( 'Set selected index to -1', () => {
|
||||
const title = 'dummy';
|
||||
context.state.title = title;
|
||||
actions.closeQuickView( context, title );
|
||||
|
||||
expect( actions.commit ).toHaveBeenCalled();
|
||||
expect( actions.commit ).toHaveBeenCalledWith( 'SET_SELECTED_INDEX', -1 );
|
||||
|
||||
} );
|
||||
|
||||
it( 'Removes QuickView to history state', () => {
|
||||
const title = 'dummy';
|
||||
context.state.title = title;
|
||||
actions.closeQuickView( context, title );
|
||||
|
||||
expect( window.history.pushState ).toHaveBeenCalled();
|
||||
expect( window.history.pushState ).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining(
|
||||
{ quickView: title }
|
||||
),
|
||||
null,
|
||||
expect.anything()
|
||||
);
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
正在加载...
在新工单中引用