Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
68
gaia/dev_apps/music-oga/test/marionette/lib/fakeactivitycaller.js
Executable file
68
gaia/dev_apps/music-oga/test/marionette/lib/fakeactivitycaller.js
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
/* global module */
|
||||
'use strict';
|
||||
|
||||
function FakeActivityCaller(client, origin) {
|
||||
this.client = client;
|
||||
this.origin = origin || ('app://' + FakeActivityCaller.DEFAULT_ORIGIN);
|
||||
}
|
||||
|
||||
module.exports = FakeActivityCaller;
|
||||
|
||||
FakeActivityCaller.DEFAULT_ORIGIN = 'fakeactivity.gaiamobile.org';
|
||||
|
||||
FakeActivityCaller.Selector = Object.freeze({
|
||||
openButton: '#open',
|
||||
pickButton: '#pick',
|
||||
|
||||
actionMenu: 'form[data-z-index-level="action-menu"]'
|
||||
});
|
||||
|
||||
FakeActivityCaller.prototype = {
|
||||
client: null,
|
||||
|
||||
get openButton() {
|
||||
return this.client.helper.waitForElement(
|
||||
FakeActivityCaller.Selector.openButton);
|
||||
},
|
||||
|
||||
get pickButton() {
|
||||
return this.client.helper.waitForElement(
|
||||
FakeActivityCaller.Selector.pickButton);
|
||||
},
|
||||
|
||||
get actionMenu() {
|
||||
return this.client.helper.waitForElement(
|
||||
FakeActivityCaller.Selector.actionMenu);
|
||||
},
|
||||
|
||||
launch: function() {
|
||||
this.client.switchToFrame();
|
||||
this.client.apps.launch(this.origin);
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.client.apps.close(this.origin);
|
||||
},
|
||||
|
||||
tapOpenButton: function() {
|
||||
this.openButton.tap();
|
||||
},
|
||||
|
||||
tapPickButton: function() {
|
||||
this.pickButton.tap();
|
||||
},
|
||||
|
||||
selectMusicApp: function() {
|
||||
this.client.switchToFrame();
|
||||
|
||||
var list = this.actionMenu.findElements('button');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].text() === 'Music OGA') {
|
||||
this.client.helper.waitForElement(list[i]);
|
||||
list[i].tap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
37
gaia/dev_apps/music-oga/test/marionette/lib/fakecontrols.js
Executable file
37
gaia/dev_apps/music-oga/test/marionette/lib/fakecontrols.js
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
/* global module */
|
||||
'use strict';
|
||||
|
||||
function FakeControls(client, origin) {
|
||||
this.client = client;
|
||||
this.origin = origin || ('app://' + FakeControls.DEFAULT_ORIGIN);
|
||||
}
|
||||
|
||||
module.exports = FakeControls;
|
||||
|
||||
FakeControls.DEFAULT_ORIGIN = 'fakecontrols.gaiamobile.org';
|
||||
|
||||
FakeControls.Selector = Object.freeze({
|
||||
playPause: '#playpause'
|
||||
});
|
||||
|
||||
FakeControls.prototype = {
|
||||
client: null,
|
||||
|
||||
get playPauseElement() {
|
||||
return this.client.findElement(FakeControls.Selector.playPause);
|
||||
},
|
||||
|
||||
launch: function() {
|
||||
this.client.switchToFrame();
|
||||
this.client.apps.launch(this.origin);
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.client.apps.close(this.origin);
|
||||
},
|
||||
|
||||
playPause: function() {
|
||||
this.playPauseElement.click();
|
||||
}
|
||||
};
|
||||
25
gaia/dev_apps/music-oga/test/marionette/lib/fakeringtones.js
Executable file
25
gaia/dev_apps/music-oga/test/marionette/lib/fakeringtones.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
/* global module */
|
||||
'use strict';
|
||||
|
||||
function FakeRingtones(client, origin) {
|
||||
this.client = client;
|
||||
this.origin = origin || ('app://' + FakeRingtones.DEFAULT_ORIGIN);
|
||||
}
|
||||
|
||||
module.exports = FakeRingtones;
|
||||
|
||||
FakeRingtones.DEFAULT_ORIGIN = 'fakeringtones.gaiamobile.org';
|
||||
|
||||
FakeRingtones.prototype = {
|
||||
client: null,
|
||||
|
||||
launch: function() {
|
||||
this.client.switchToFrame();
|
||||
this.client.apps.launch(this.origin);
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.client.apps.close(this.origin);
|
||||
}
|
||||
};
|
||||
22
gaia/dev_apps/music-oga/test/marionette/lib/listviewhelper.js
Executable file
22
gaia/dev_apps/music-oga/test/marionette/lib/listviewhelper.js
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
/* global module, require */
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var ListviewHelper = {
|
||||
|
||||
albumArtForListItem: function(client, item) {
|
||||
assert.ok(item);
|
||||
|
||||
var src;
|
||||
client.waitFor(function() {
|
||||
src = item.findElement('.list-album-art').getAttribute('src');
|
||||
return src !== null;
|
||||
});
|
||||
return src;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = ListviewHelper;
|
||||
397
gaia/dev_apps/music-oga/test/marionette/lib/music.js
Executable file
397
gaia/dev_apps/music-oga/test/marionette/lib/music.js
Executable file
|
|
@ -0,0 +1,397 @@
|
|||
/* global require, module */
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
function Music(client, origin) {
|
||||
this.client = client;
|
||||
this.origin = origin || ('app://' + Music.DEFAULT_ORIGIN);
|
||||
this.actions = client.loader.getActions();
|
||||
}
|
||||
|
||||
module.exports = Music;
|
||||
|
||||
Music.DEFAULT_ORIGIN = 'music-oga.gaiamobile.org';
|
||||
|
||||
Music.Selector = Object.freeze({
|
||||
messageOverlay: '#overlay',
|
||||
firstTile: '.tile',
|
||||
playlistsTab: '#tabs-playlists',
|
||||
artistsTab: '#tabs-artists',
|
||||
songsTab: '#tabs-songs',
|
||||
albumsTab: '#tabs-albums',
|
||||
playerCover: '#player-cover',
|
||||
|
||||
// search fields
|
||||
searchField: '#views-search-input',
|
||||
searchTiles: '#views-tiles-search',
|
||||
searchTilesField: '#views-tiles-search-input',
|
||||
searchList: '#views-list-search',
|
||||
searchListField: '#views-list-search-input',
|
||||
// search results
|
||||
searchArtists: '#views-search-artists',
|
||||
searchAlbums: '#views-search-albums',
|
||||
searchTitles: '#views-search-titles',
|
||||
searchNoResult: '#views-search-no-result',
|
||||
|
||||
tilesView: '#views-tiles',
|
||||
listView: '#views-list',
|
||||
viewsList: '#views-list-anchor',
|
||||
sublistView: '#views-sublist',
|
||||
viewsSublist: '#views-sublist-anchor',
|
||||
sublistViewControls: '#views-sublist-header-controls',
|
||||
firstListItem: '.list-item',
|
||||
firstSong: '.list-item',
|
||||
firstSongSublist: '#views-sublist .list-item',
|
||||
playerView: '#views-player',
|
||||
playButton: '#player-controls-play',
|
||||
progressBar: '#player-seek-bar-progress',
|
||||
shareButton: '#player-cover-share',
|
||||
ratingBar: '#player-album-rating',
|
||||
ratingStarsOn: 'button.star-on',
|
||||
shareMenu: 'form[data-z-index-level="action-menu"]',
|
||||
pickDoneButton: '#title-done',
|
||||
header: '#title',
|
||||
titleText: '#title-text',
|
||||
sublistShuffleButton: '#views-sublist-controls-shuffle',
|
||||
playerIcon: '#title-player'
|
||||
});
|
||||
|
||||
Music.prototype = {
|
||||
client: null,
|
||||
|
||||
get messageOverlay() {
|
||||
return this.client.findElement(Music.Selector.messageOverlay);
|
||||
},
|
||||
|
||||
get firstTile() {
|
||||
return this.client.findElement(Music.Selector.firstTile);
|
||||
},
|
||||
|
||||
get playlistsTab() {
|
||||
return this.client.helper.waitForElement(Music.Selector.playlistsTab);
|
||||
},
|
||||
|
||||
get artistsTab() {
|
||||
return this.client.helper.waitForElement(Music.Selector.artistsTab);
|
||||
},
|
||||
|
||||
get songsTab() {
|
||||
return this.client.helper.waitForElement(Music.Selector.songsTab);
|
||||
},
|
||||
|
||||
get albumsTab() {
|
||||
return this.client.helper.waitForElement(Music.Selector.albumsTab);
|
||||
},
|
||||
|
||||
get firstSong() {
|
||||
return this.client.helper.waitForElement(Music.Selector.firstSong);
|
||||
},
|
||||
|
||||
get firstSongSublist() {
|
||||
return this.client.helper.waitForElement(Music.Selector.firstSongSublist);
|
||||
},
|
||||
|
||||
// Helper for the getter.
|
||||
_getListItems: function(selector) {
|
||||
this.waitForAList(selector);
|
||||
|
||||
var list = this.client.findElement(selector);
|
||||
assert.ok(list, 'Couldn\'t find element ' + selector);
|
||||
|
||||
var list_items = list.findElements('li.list-item', 'css selector');
|
||||
assert.ok(list_items, 'Coudln\'t find list-items for ' + selector);
|
||||
|
||||
return list_items;
|
||||
},
|
||||
|
||||
get firstListItem() {
|
||||
return this.client.helper.waitForElement(Music.Selector.firstListItem);
|
||||
},
|
||||
|
||||
get listItems() {
|
||||
return this._getListItems(Music.Selector.viewsList);
|
||||
},
|
||||
|
||||
get songs() {
|
||||
return this._getListItems(Music.Selector.viewsSublist);
|
||||
},
|
||||
|
||||
get playButton() {
|
||||
return this.client.findElement(Music.Selector.playButton);
|
||||
},
|
||||
|
||||
get shareButton() {
|
||||
return this.client.findElement(Music.Selector.shareButton);
|
||||
},
|
||||
|
||||
get title() {
|
||||
var header = this.client.findElement(Music.Selector.header);
|
||||
return header.findElement(Music.Selector.titleText);
|
||||
},
|
||||
|
||||
get sublistShuffleButton() {
|
||||
return this.client.helper.waitForElement(
|
||||
Music.Selector.sublistShuffleButton);
|
||||
},
|
||||
|
||||
// TODO(gareth): Move this shareMenu stuff into the helper.
|
||||
get shareMenu() {
|
||||
// Switch to the system app first.
|
||||
this.client.switchToFrame();
|
||||
return this.client.helper.waitForElement(Music.Selector.shareMenu);
|
||||
},
|
||||
|
||||
get progressBar() {
|
||||
return this.client.findElement(Music.Selector.progressBar);
|
||||
},
|
||||
|
||||
get pickDoneButton() {
|
||||
return this.client.findElement(Music.Selector.pickDoneButton);
|
||||
},
|
||||
|
||||
get header() {
|
||||
return this.client.findElement(Music.Selector.header);
|
||||
},
|
||||
|
||||
get playerIcon() {
|
||||
return this.client.findElement(Music.Selector.playerIcon);
|
||||
},
|
||||
|
||||
get isPlaying() {
|
||||
return this.playButton.getAttribute('class').indexOf('is-pause') === -1;
|
||||
},
|
||||
|
||||
get songProgress() {
|
||||
return parseFloat(this.progressBar.getAttribute('value'));
|
||||
},
|
||||
|
||||
launch: function() {
|
||||
this.client.switchToFrame();
|
||||
this.client.apps.launch(this.origin);
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
this.client.helper.waitForElement('body');
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.client.apps.close(this.origin);
|
||||
},
|
||||
|
||||
switchToMe: function(options) {
|
||||
options = options || {};
|
||||
|
||||
this.client.switchToFrame();
|
||||
|
||||
// Switch to music even when it is in background.
|
||||
// We cannot use switchToApp here, because it will wait
|
||||
// for the app to come to foreground.
|
||||
if (options.background) {
|
||||
var musicFrame = this.client.findElement('iframe[src*="' +
|
||||
this.origin + '"]');
|
||||
this.client.switchToFrame(musicFrame);
|
||||
} else {
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
}
|
||||
},
|
||||
|
||||
waitForListEnumerate: function() {
|
||||
this.client.waitFor(function() {
|
||||
return this.client.executeScript(function() {
|
||||
return window.wrappedJSObject.ListView.handle.state === 'complete';
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
waitFinishedScanning: function() {
|
||||
this.client.waitFor(function() {
|
||||
return this.client.executeScript(function() {
|
||||
return window.wrappedJSObject.Database.initialScanComplete === true;
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
waitForFirstTile: function() {
|
||||
this.client.helper.waitForElement(Music.Selector.firstTile);
|
||||
},
|
||||
|
||||
waitForMessageOverlayShown: function(shouldBeShown) {
|
||||
this.client.waitFor(function() {
|
||||
var volumeShown = this.messageOverlay.displayed();
|
||||
return volumeShown === shouldBeShown;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
waitForAList: function(selector) {
|
||||
this.client.waitFor(function() {
|
||||
return this.client.findElement(selector).displayed();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
// In sublist view.
|
||||
// XXX allow in list view too.
|
||||
waitForSongs: function(callback) {
|
||||
this.client.waitFor(function() {
|
||||
var songs = this.songs;
|
||||
return callback(songs);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
waitForListView: function() {
|
||||
this.client.helper.waitForElement(Music.Selector.listView);
|
||||
},
|
||||
|
||||
waitForSubListView: function() {
|
||||
this.client.helper.waitForElement(Music.Selector.sublistView);
|
||||
},
|
||||
|
||||
waitForPlayerView: function() {
|
||||
this.client.helper.waitForElement(Music.Selector.playerView);
|
||||
},
|
||||
|
||||
// Because bug 862156 so we couldn't get the correct displayed value for the
|
||||
// player icon, instead we use the display property to check the visibility
|
||||
// of the player icon.
|
||||
checkPlayerIconShown: function(shouldBeShown) {
|
||||
var display = this.playerIcon.cssProperty('display');
|
||||
var result = (display !== 'none');
|
||||
assert.equal(shouldBeShown, result);
|
||||
},
|
||||
|
||||
showSearchInput: function(viewSelector) {
|
||||
var tilesView = this.client.findElement(viewSelector);
|
||||
var chain = this.actions.press(tilesView, 10, 10).perform();
|
||||
chain.moveByOffset(0, 110).perform();
|
||||
chain.release().perform();
|
||||
},
|
||||
|
||||
searchArtists: function(searchTerm) {
|
||||
this.search(Music.Selector.searchList, searchTerm);
|
||||
},
|
||||
|
||||
searchTiles: function(searchTerm) {
|
||||
this.search(Music.Selector.searchTiles, searchTerm);
|
||||
},
|
||||
|
||||
search: function(viewSelector, searchTerm) {
|
||||
this.client.findElement(viewSelector).tap();
|
||||
|
||||
var input = this.client.helper.waitForElement(Music.Selector.searchField);
|
||||
assert.ok(input);
|
||||
|
||||
input.clear();
|
||||
this.client.waitFor(input.displayed.bind(input));
|
||||
input.sendKeys(searchTerm);
|
||||
},
|
||||
|
||||
switchToArtistsView: function() {
|
||||
this.artistsTab.tap();
|
||||
},
|
||||
|
||||
switchToSongsView: function() {
|
||||
this.songsTab.tap();
|
||||
},
|
||||
|
||||
switchToAlbumsView: function() {
|
||||
this.albumsTab.tap();
|
||||
},
|
||||
|
||||
switchToPlaylistsView: function() {
|
||||
this.playlistsTab.tap();
|
||||
},
|
||||
|
||||
selectAlbum: function(name) {
|
||||
var list_items = this.listItems;
|
||||
|
||||
list_items.filter(function (element) {
|
||||
return element.findElement('.list-main-title', 'css selector')
|
||||
.text() === name;
|
||||
})[0].tap();
|
||||
},
|
||||
|
||||
selectArtist: function(name) {
|
||||
var list_items = this.listItems;
|
||||
|
||||
list_items.filter(function (element) {
|
||||
return element.findElement('.list-single-title', 'css selector')
|
||||
.text() === name;
|
||||
})[0].tap();
|
||||
},
|
||||
|
||||
selectPlaylist: function(name) {
|
||||
var list_items = this.listItems;
|
||||
|
||||
list_items.filter(function (element) {
|
||||
return element.findElement('.list-playlist-title', 'css selector')
|
||||
.text() === name;
|
||||
})[0].tap();
|
||||
},
|
||||
|
||||
// only from a list (song list)
|
||||
playFirstSong: function() {
|
||||
this.firstSong.click();
|
||||
},
|
||||
|
||||
// only from a sublist (artist, albums, playlists)
|
||||
playFirstSongSublist: function() {
|
||||
this.firstSongSublist.click();
|
||||
},
|
||||
|
||||
tapPlayButton: function() {
|
||||
this.actions.tap(this.playButton).perform();
|
||||
},
|
||||
|
||||
tapHeaderActionButton: function() {
|
||||
this.header.tap(25, 25);
|
||||
},
|
||||
|
||||
showSongInfo: function() {
|
||||
this.client.helper.waitForElement(Music.Selector.playerCover).click();
|
||||
},
|
||||
|
||||
tapRating: function(rating) {
|
||||
this.showSongInfo();
|
||||
this.client.helper.waitForElement('button.rating-star[data-rating="' +
|
||||
rating + '"]').tap();
|
||||
},
|
||||
|
||||
shareWith: function(appName) {
|
||||
|
||||
// Allow findElement to fail quickly.
|
||||
var quickly = this.client.scope({
|
||||
searchTimeout: 50
|
||||
});
|
||||
|
||||
var shareMenu;
|
||||
// Wait until the share menu is displayed.
|
||||
// Try to click the cover image followed by the share button in the case
|
||||
// that it hides before we get a chance to click it.
|
||||
this.client.waitFor(function() {
|
||||
this.showSongInfo();
|
||||
this.shareButton.tap();
|
||||
|
||||
this.client.switchToFrame();
|
||||
try {
|
||||
shareMenu = quickly.findElement(Music.Selector.shareMenu);
|
||||
} catch(e) {
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
return false;
|
||||
}
|
||||
|
||||
var isDisplayed = shareMenu.displayed();
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
return isDisplayed;
|
||||
}.bind(this));
|
||||
|
||||
var list = this.shareMenu.findElements('button');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].text() === appName) {
|
||||
list[i].tap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
finishPick: function() {
|
||||
this.actions.tap(this.pickDoneButton).perform();
|
||||
}
|
||||
};
|
||||
28
gaia/dev_apps/music-oga/test/marionette/lib/playerhelper.js
Executable file
28
gaia/dev_apps/music-oga/test/marionette/lib/playerhelper.js
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
/* global module, require */
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var PlayerHelper = {
|
||||
|
||||
// check the order of the rating stars.
|
||||
checkRatingStarsOrder: function(stars) {
|
||||
var expected = 1;
|
||||
stars.forEach(function(element) {
|
||||
// we always assume the stars are in DOM order. There is no reason
|
||||
// they wouldn't be.
|
||||
var value = element.getAttribute('data-rating');
|
||||
assert.equal(value, expected);
|
||||
expected++;
|
||||
});
|
||||
},
|
||||
|
||||
checkEmptyRating: function(stars) {
|
||||
stars.forEach(function(elem) {
|
||||
assert.equal(elem.getAttribute('class').indexOf('star-on'), -1,
|
||||
'Check empty rating. Star is on. Should be off');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PlayerHelper;
|
||||
22
gaia/dev_apps/music-oga/test/marionette/lib/playlisthelper.js
Executable file
22
gaia/dev_apps/music-oga/test/marionette/lib/playlisthelper.js
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
/* global module */
|
||||
'use strict';
|
||||
|
||||
|
||||
var PlaylistHelper = {
|
||||
|
||||
mainTitle: function(element) {
|
||||
return element.findElement('.list-main-title').text();
|
||||
},
|
||||
|
||||
songTitle: function(element) {
|
||||
return element.findElement('.list-song-title').text();
|
||||
},
|
||||
|
||||
songIndex: function(element) {
|
||||
return element.findElement('.list-song-index').text();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = PlaylistHelper;
|
||||
22
gaia/dev_apps/music-oga/test/marionette/lib/searchhelper.js
Executable file
22
gaia/dev_apps/music-oga/test/marionette/lib/searchhelper.js
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
/* global module */
|
||||
'use strict';
|
||||
|
||||
|
||||
var SearchHelper = {
|
||||
|
||||
singleTitle: function(element) {
|
||||
return element.findElement('.list-single-title').text();
|
||||
},
|
||||
|
||||
mainTitle: function(element) {
|
||||
return element.findElement('.list-main-title').text();
|
||||
},
|
||||
|
||||
highlight: function(element) {
|
||||
return element.findElement('.search-highlight').text();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = SearchHelper;
|
||||
28
gaia/dev_apps/music-oga/test/marionette/lib/statusbar.js
Executable file
28
gaia/dev_apps/music-oga/test/marionette/lib/statusbar.js
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
/* global module */
|
||||
'use strict';
|
||||
|
||||
function Statusbar(client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
module.exports = Statusbar;
|
||||
|
||||
Statusbar.Selector = Object.freeze({
|
||||
playingIndicator: '#statusbar-playing'
|
||||
});
|
||||
|
||||
Statusbar.prototype = {
|
||||
client: null,
|
||||
|
||||
get playingIndicator() {
|
||||
this.client.switchToFrame();
|
||||
return this.client.findElement(Statusbar.Selector.playingIndicator);
|
||||
},
|
||||
|
||||
waitForPlayingIndicatorShown: function(shouldBeShown) {
|
||||
this.client.waitFor(function() {
|
||||
var shown = this.playingIndicator.displayed();
|
||||
return shown === shouldBeShown;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue