Testing a BLoC with StreamSubscription in Flutter - unit-testing

I'm testing the blocs of my Flutter application. I'm using flutter_bloc, bloc_test and mockito libraries in my project.
One of my blocs uses a repository with a method that returns a Stream of objects. I don't understand how to mock the repository and the StreamSubscription inside the bloc.
Here is the code for my BLoC:
class StopsTrackerBloc extends Bloc<StopsTrackerEvent, StopsTrackerState> {
final LocationRepository locationRepository;
final List<Stop> stops;
StreamSubscription _locationSubscription;
StopsTrackerBloc({
#required this.locationRepository,
#required this.stops,
}) : assert(locationRepository != null),
assert(stops != null),
super(Ready(stops));
#override
Stream<StopsTrackerState> mapEventToState(StopsTrackerEvent event) async* {
if (event is StartTracking) {
yield* _mapStartTrackingToState();
} else if (event is NewStopRecorded) {
yield* _mapNewStopRecordedToState(event.stop);
} else if (event is StopTracking) {
yield* _mapStopTrackingToState();
}
}
// Cancels the _locationSubscription when the StopsTrackerBloc is closed.
#override
Future<void> close() {
_locationSubscription?.cancel();
return super.close();
}
Stream<StopsTrackerState> _mapStartTrackingToState() async* {
_locationSubscription?.cancel();
_locationSubscription =
locationRepository.locationStream.listen((Stop stop) {
add(NewStopRecorded(stop: stop));
});
}
Stream<StopsTrackerState> _mapNewStopRecordedToState(Stop stop) async* {
stops.add(stop);
yield Running(stops);
}
Stream<StopsTrackerState> _mapStopTrackingToState() async* {
_locationSubscription?.cancel();
yield Paused(stops);
}
}
Here I tried running these tests:
class MockLocationRepository extends Mock implements LocationRepository {}
void main() {
LocationRepository locationRepository;
StopsTrackerBloc stopsTrackerBloc;
final List<Stop> initialStops = [
Stop(
coords: Coordinates(latitude: 1111, longitude: 1111),
time: DateTime.now(),
),
Stop(
coords: Coordinates(latitude: 2222, longitude: 2222),
time: DateTime.now(),
),
Stop(
coords: Coordinates(latitude: 3333, longitude: 3333),
time: DateTime.now(),
),
Stop(
coords: Coordinates(latitude: 4444, longitude: 4444),
time: DateTime.now(),
),
];
setUp(() {
locationRepository = MockLocationRepository();
stopsTrackerBloc = StopsTrackerBloc(
locationRepository: locationRepository,
stops: initialStops,
);
});
tearDown(() {
stopsTrackerBloc?.close();
});
group('StartTracking', () {
final Stop newStop = Stop(
coords: Coordinates(latitude: 5555, longitude: 5555),
time: DateTime.now(),
);
blocTest(
'emits [Running] and adds a new stop to the initial list of stops',
build: () async {
when(locationRepository.locationStream)
.thenAnswer((_) => Stream.fromIterable([newStop]));
return stopsTrackerBloc;
},
act: (bloc) => bloc.add(StartTracking()),
expect: [Running(initialStops..add(newStop))],
);
});
}
Result of the test:
StartTracking emits [Running] and adds a new stop to the initial list of stops:
ERROR: Expected: [
Running:Running { stops: [ Stop {
location: Coordinates { latitude: 1111.0, longitude: 1111.0 },
time: 2020-07-08 15:06:48.934837,
}, Stop {
location: Coordinates { latitude: 2222.0, longitude: 2222.0 },
time: 2020-07-08 15:06:48.934837,
}, Stop {
location: Coordinates { latitude: 3333.0, longitude: 3333.0 },
time: 2020-07-08 15:06:48.934837,
}, Stop {
location: Coordinates { latitude: 4444.0, longitude: 4444.0 },
time: 2020-07-08 15:06:48.934837,
}, Stop {
location: Coordinates { latitude: 5555.0, longitude: 5555.0 },
time: 2020-07-08 15:06:48.942809,
}] }
]
Actual: []
Which: shorter than expected at location [0]
package:test_api expect
package:bloc_test/src/bloc_test.dart 143:29 blocTest.<fn>.<fn>
===== asynchronous gap ===========================
dart:async _asyncThenWrapperHelper
package:bloc_test/src/bloc_test.dart blocTest.<fn>.<fn>
dart:async runZoned
package:bloc_test/src/bloc_test.dart 135:11 blocTest.<fn>
I don't understand why the Actual emitted states is an empty list!

Related

chart js not dispalying data array that comes from an axios request

I have an API end point that returns an array of 24 values that I want to use in my chartjs within a vue component.
when the page loads I get no errors but the bars on the charts just don't show and I don't know why.
EDIT: I noticed that the async function returns a promise instead of the actual data:
async filterData() {
await this.$axios.get('/api/data_app/job_count_by_hour/')
.then(response => {
return this.chart_data = response.data;
})
}
here is the data return code, I have a function that populates the chart_data array :
data(){
return {
form:{
day: 'select day',
workspace:'',
machine_family: [],
duration: []
},
res: [],
total:[],
chart_data: [],
url: '/api/jobs/job_count_by_hour/',
days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "sunday"],
barChart2: {
labels: ["6h", "7h", "8h", "9h","10h","11h", "12h", "13h", "14h", "15h", "16h", "17h", "18h", "19h", "20h", "21h","22h", "23h", "00h"],
datasets: [{
label: ["popularity"],
backgroundColor:"#f93232" ,
data: this.chart_data
},],
},
}
},
methods: {
async filterData() {
let _url = `${this.url}`
await this.$axios.get(_url)
.then(response => {
this.chart_data = response.data;
})
return this.chart_data
},
},
mounted() {
this.filterData()
}
}
this is the chart component:
<script>
import { Line } from 'vue-chartjs'
export default {
extends: Line,
props: {
chartdata: {
type: Object,
default: null
},
options: {
type: Object,
default: null
}
},
mounted () {
this.renderChart(this.chartdata, this.options)
}
}
in the parent component It looks like this:
en <BarChart :labels="barChart2.labels"
:datasets="barChart2.datasets"
:height="100"
>
</BarChart>ter code here
Turns out that when you try to update nested data, the component doesn't re-render.
This is how I solved it, I put the entire object in an update function and call that function when i get my data from the back end, I hope this helps!:
methods: {
onInput(value) {
this.filterData()
},
updateChart(data) {
this.datasets = [{
label: ["popularity"],
backgroundColor:"#f93232",
data: data
}]
},
async loadData() {
await this.$axios.get(this.url)
.then(response => {
this.updateChart(response.data)
})
},
},
mounted() {
this.loadData()
},

Error Cannot tween a null target in Unit test with GSAP-TweenMax, Jest and VueJs

I have an error when trying to perform a unit test with Jest on a component in VueJs that has an animation made with TweenMax GSAP.
The error is: Cannot tween a null target.
in ztButton.spec.js
jest.mock('gsap/TweenMax')
it('Component must to emit event on click', () => {
const wrapper = shallowMount(ztButton)
const spy = sinon.spy()
wrapper.setMethods({ clickButton: spy })
wrapper.find('.zt-button').trigger('click')
expect(spy.called).toBe(true)
})
in my project directory
in TweenMax.js of mock directory
module.exports = {
TweenMax: class {
static to(selector, time, options) {
return jest.fn()
}
}
}
in test directory
There is something I do not understand, or that I am not doing well. Something confused.
Update:
This is what I do to generate an animation in my component and is invoked in mounted
mounted() {
this.componentId = this._uid
this._addButtonRipple()
},
methods: {
_addButtonRipple() {
const $button = this.$refs.button
$button.addEventListener('click', event => {
const rect = $button.getBoundingClientRect(),
x = event.clientX - rect.left,
y = event.clientY - rect.top
let $ripple = $button.querySelector('.zt-button-ripple')
TweenMax.set($ripple, {
x: x,
y: y,
scaleX: 0,
scaleY: 0,
opacity: 1
})
TweenMax.to($ripple, 1.5, {
scaleX: 1,
scaleY: 1,
opacity: 0,
ease: Expo.easeOut
})
})
},
clickButton(event) {
this.$emit('click', event)
this.isRipple = true
setTimeout(() => {
this.isRipple = false
}, 300)
}
}
in computed
computed: {
listeners() {
return {
...this.$listeners,
click: event => this.clickButton(event)
}
}
in html tags
<button v-on="listeners"></button>
this is the configuration of my file jest.config.js
module.exports = {
verbose: true,
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
transformIgnorePatterns: ['/node_modules/'],
moduleNameMapper: {
'^#/(.*)$': '<rootDir>/src/$1',
'^src/(.*)$': '<rootDir>/src/$1',
'^src/component/(.*)$': '<rootDir>/src/components/atomic/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/components/atomic/**/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/'
}
Cannot tween a null target means that TweenMax methods are not mocked. Infact you are mocking just TweenMax.to method.
Please update your mock this way:
module.exports = {
TweenMax: class {
static to(selector, time, options) {
return jest.fn()
}
static set(selector, options) {
return jest.fn()
}
}
}
Let me know if that fixes it.

Ionic V2 creating a prompt alert

Here's my code:
public add() {
let alert = Alert.create({
title: "Add Date & Time",
message: "Enter the date and time of your donation.",
inputs: [
{
name: "date",
placeholder: "DD/MM/YYYY"
},
{
name: "time",
placeholder: "HH:MM AM/PM"
}
],
buttons: [
{
text: "Cancel"
},
{
text: "Save",
handler: data => {
this.donationHistoryList.push({
date: data.date,
time: data.time
});
}
}
]
});
this.navCtrl.present(alert);
}
Here are the errors I am getting
Property 'create' does not exist on 'type of Alert'.
And
Property 'present' does not exist on 'type of 'NavController'.
Use this code.
import {AlertController} from 'ionic-angular';
In your constructor fn
constructor(private alertCtrl: AlertController) {
}
public add() {
let alert = alertCtrl.create({
title: "Add Date & Time",
message: "Enter the date and time of your donation.",
inputs: [ { name: "date", placeholder: "DD/MM/YYYY" }, { name: "time", placeholder: "HH:MM AM/PM" } ],
buttons: [ { text: "Cancel" }, { text: "Save", handler: data => { this.donationHistoryList.push({ date: data.date, time: data.time }); } }]
});
alert.present(); }
Hope this helps you. Thanks.
Just add (this before alertCtrl variable).
this.alertCtrl.create(...)

Show a confirmation alert before app close ionic 2

I make one application with ionic 2. I am trying to get a confirmation alert before close the application.
How can I do it ?
export class MyApp{
constructor(public alert: AlertController,public platform: Platform){}
exit(){
let alert = this.alert.create({
title: 'Confirm',
message: 'Do you want to exit?',
buttons: [{
text: "exit?",
handler: () => { this.exitApp() }
}, {
text: "Cancel",
role: 'cancel'
}]
})
alert.present();
}
exitApp(){
this.platform.exitApp();
}
}
If you would like to enable back button exit, add event listener for it and call exit function.
You can use this.platform.registerBackButtonAction(this.exit) for it.
I could find by myself the right solution:
https://forum.ionicframework.com/t/show-a-confirmation-alert-before-app-close-ionic/63313
showedAlert: boolean;
constructor(..., public alertCtrl: AlertController) {
}
initializeApp() {
this.platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
StatusBar.styleDefault();
Splashscreen.hide();
this.showedAlert = false;
// Confirm exit
this.platform.registerBackButtonAction(() => {
if (this.nav.length() == 1) {
if (!this.showedAlert) {
this.confirmExitApp();
} else {
this.showedAlert = false;
this.confirmAlert.dismiss();
}
}
this.nav.pop();
});
});
}
confirmExitApp() {
this.showedAlert = true;
this.confirmAlert = this.alertCtrl.create({
title: "Salir",
message: "¿ Esta seguro que desea salir de la aplicación ?",
buttons: [
{
text: 'Cancelar',
handler: () => {
this.showedAlert = false;
return;
}
},
{
text: 'Aceptar',
handler: () => {
this.platform.exitApp();
}
}
]
});
this.confirmAlert.present();
}
Ionic 2+ quick solution: In your app.component.ts try
ngOnInit() {
this.platform.registerBackButtonAction(() => {
if (this.nav.canGoBack()) {
this.nav.pop();
} else {
// Currently on root page
this.appClosePromt();
}
}, 1);
}
appClosePromt() {
let alert = this.alertCtrl.create({
title: '',
message: 'Do you want to exit?',
buttons: [
{
text: 'No',
role: 'cancel',
handler: () => {
// Dismiss
}
},
{
text: 'Exit',
handler: () => {
this.platform.exitApp();
}
}
]
});
alert.present();
}

Sencha Touch 2 list/store

I have a problem with an list-component on the sencha touch 2 framework. My problem is, that my list doesn't show the calculated distance between the current position and the places.
First I use an navigation view with a list (name + distance) when the user click the name more details about them appears (+ the back-button will generated automatic). If the back-button is pressed the list shows the correct distance in the list. But I really need the distance is shown at the first time. I tried a lot but nothing helps.
I use a model and a store:
'Ext.define('Guide.store.ProjekteList', {
extend: 'Ext.data.Store',
config:{
model: "Guide.model.ProjekteList",
autoLoad:true,
sorters: ['distance'],
storeId: 'ProjekteList',
proxy: {
type: 'ajax',
url : 'PHP/get_MainList.php',
reader: {
type: 'json',
rootProperty:'items'
}
},
listeners: {
load : function(){
this.each(function(store){
var newData = getDis(store.data);
});//each
}//load func
}// listener
}//config
});// klasse
var getDis = function(dataset) {
var geo = Ext.create('Ext.util.Geolocation', {
autoUpdate: false,
listeners: {
locationupdate: function(geo) {
polat = geo.getLatitude();
polng = geo.getLongitude();
var B1 = dataset.Lat / 180 * Math.PI;
var B2 = polat / 180 * Math.PI;
var L1 = dataset.Lng / 180 * Math.PI;
var L2 = polng / 180 * Math.PI;
var zwi = Math.acos(Math.sin(B1)*Math.sin(B2) + Math.cos(B1)*Math.cos(B2)*Math.cos(L2-L1));
var r = 6378.137; //km
dataset.distance = r * zwi;
dataset.distance = Math.round(dataset.distance*100)/100;
},
locationerror: function(geo, bTimeout, bPermissionDenied, bLocationUnavailable, message) {
if(bTimeout){
alert('Timeout occurred.');
} else {
alert('Error occurred.');
}
}
}
});
geo.updateLocation();
return dataset;
};'
and my model:
'Ext.define('Guide.model.ProjekteList', {
extend: 'Ext.data.Model',
config: {
fields: ['Projektname', 'Lat', 'Lng', 'distance', 'ID'],
}
});
here is my navigation view:
'Ext.define('Guide.view.ProjekteList', {
extend: 'Ext.navigation.View',
xtype: 'projektelist',
config: {
title: 'Orte',
iconCls:'Projekte' ,
id: 'listButton',
items:[
{
xtype: 'list',
onItemDisclosure: true,
/* plugins: [
{
xclass: 'Ext.plugin.ListPaging',
autoPaging: false,
}
], */
title: 'Sehenswerte Orte',
store: 'ProjekteList',
itemId: 'liste',
itemTpl: '<h2>{Projektname}</h2> Entfernung: {distance} km',
listeners: {
show: function(){
this.refresh();
} //show function
}, //listeners
} // item
] //items
}// config
}); '
and my controller
Ext.define("Guide.controller.ProjekteList", {
extend: "Ext.app.Controller",
views: ['ProjektList'],
config: {
refs: {
projekt: 'projektelist',
},
control: {
'projektelist list': {
itemtap: 'showDetail'
}
}
},
showDetail: function(list, index, element, record) {
var projektid = record.get('ID');
Ext.StoreMgr.get('ProjektDetail').setProxy({url:'PHP/get_Detail.php?
ID='+projektid}).load();
this.getProjekt().push({
xtype: 'projektdetails',
});
} // showDetail function
});
Thanks in advance!
I found the solution:
only change the listener in the navigation view from "show" to "painted" and it works fine.