Description: <short summary of the patch>
 TODO: Put a short summary on the line above and replace this paragraph
 with a longer explanation of this change. Complete the meta-information
 with other relevant fields (see below for details). To make it easier, the
 information below has been extracted from the changelog. Adjust it or drop
 it.
 .
 gnome-shell (48.0-1) unstable; urgency=medium
 .
   * New upstream release
   * Bump minimum mutter
Author: Jeremy Bícha <jbicha@ubuntu.com>

---
The information above should follow the Patch Tagging Guidelines, please
checkout https://dep.debian.net/deps/dep3/ to learn about the format. Here
are templates for supplementary fields that you might want to add:

Origin: (upstream|backport|vendor|other), (<patch-url>|commit:<commit-id>)
Bug: <upstream-bugtracker-url>
Bug-Debian: https://bugs.debian.org/<bugnumber>
Bug-Ubuntu: https://launchpad.net/bugs/<bugnumber>
Forwarded: (no|not-needed|<patch-forwarded-url>)
Applied-Upstream: <version>, (<commit-url>|commit:<commid-id>)
Reviewed-By: <name and email of someone who approved/reviewed the patch>
Last-Update: 2025-06-02

--- /dev/null
+++ gnome-shell-48.0/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+    "files.associations": {
+        "shell-global.h": "c"
+    }
+}
\ No newline at end of file
--- gnome-shell-48.0.orig/src/main.c
+++ gnome-shell-48.0/src/main.c
@@ -42,6 +42,7 @@ static char *session_mode = NULL;
 static int caught_signal = 0;
 static gboolean force_animations = FALSE;
 static char *script_path = NULL;
+static bool is_phone = FALSE;
 
 #define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1
 #define DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER 4
@@ -542,6 +543,12 @@ GOptionEntry gnome_shell_options[] = {
     "",
     NULL
   },
+  {
+  "is_phone", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+    &is_phone,
+    N_("Run in phone Mode "),
+    NULL
+  },
   { NULL }
 };
 
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/.gitlab-ci.yml
@@ -0,0 +1,16 @@
+stages:
+- test
+
+build-fedora:
+  image: fedora:latest
+  stage: test
+  before_script:
+    - dnf install -y redhat-rpm-config gcc clang meson pulseaudio-libs-devel alsa-lib-devel gtk3-devel
+  script:
+    - cd .gitlab-ci
+    - meson _build
+    - ninja -C _build
+    - rm -rf _build
+    - CC=clang meson _build
+    - ninja -C _build
+
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/.meson-subproject-wrap-hash.txt
@@ -0,0 +1 @@
+f1eb66a84735d5bc7a9f0fdbf255cc67dbfe63bd222b1ea24a5db7beac5bf6fd
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/README.md
@@ -0,0 +1,12 @@
+# libgnome-volume-control
+
+libgnome-volume-control is a copy library that's supposed to be used as
+a git sub-module. If your project uses some of libgnome-volume-control's
+strings in a user-facing manner, don't forget to add those files to your
+POTFILES.in for translation.
+
+## Projects using libgnome-volume-control
+
+- [gnome-shell](https://gitlab.gnome.org/GNOME/gnome-shell)
+- [gnome-settings-daemon](https://gitlab.gnome.org/GNOME/gnome-settings-daemon)
+- [gnome-control-center](https://gitlab.gnome.org/GNOME/gnome-control-center)
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-channel-map-private.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_CHANNEL_MAP_PRIVATE_H
+#define __GVC_CHANNEL_MAP_PRIVATE_H
+
+#include <glib-object.h>
+#include <pulse/pulseaudio.h>
+
+G_BEGIN_DECLS
+
+GvcChannelMap *         gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *map);
+const pa_channel_map *  gvc_channel_map_get_pa_channel_map      (const GvcChannelMap  *map);
+
+void                    gvc_channel_map_volume_changed          (GvcChannelMap    *map,
+                                                                 const pa_cvolume *cv,
+                                                                 gboolean          set);
+const pa_cvolume *      gvc_channel_map_get_cvolume             (const GvcChannelMap  *map);
+
+G_END_DECLS
+
+#endif /* __GVC_CHANNEL_MAP_PRIVATE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-channel-map.c
@@ -0,0 +1,246 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-channel-map.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcChannelMapPrivate
+{
+        pa_channel_map        pa_map;
+        gboolean              pa_volume_is_set;
+        pa_cvolume            pa_volume;
+        gdouble               extern_volume[NUM_TYPES]; /* volume, balance, fade, lfe */
+        gboolean              can_balance;
+        gboolean              can_fade;
+};
+
+enum {
+        VOLUME_CHANGED,
+        LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL] = { 0, };
+
+static void     gvc_channel_map_finalize   (GObject            *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcChannelMap, gvc_channel_map, G_TYPE_OBJECT)
+
+guint
+gvc_channel_map_get_num_channels (const GvcChannelMap *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), 0);
+
+        if (!pa_channel_map_valid(&map->priv->pa_map))
+                return 0;
+
+        return map->priv->pa_map.channels;
+}
+
+const gdouble *
+gvc_channel_map_get_volume (GvcChannelMap *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+        if (!pa_channel_map_valid(&map->priv->pa_map))
+                return NULL;
+
+        map->priv->extern_volume[VOLUME] = (gdouble) pa_cvolume_max (&map->priv->pa_volume);
+        if (gvc_channel_map_can_balance (map))
+                map->priv->extern_volume[BALANCE] = (gdouble) pa_cvolume_get_balance (&map->priv->pa_volume, &map->priv->pa_map);
+        else
+                map->priv->extern_volume[BALANCE] = 0;
+        if (gvc_channel_map_can_fade (map))
+                map->priv->extern_volume[FADE] = (gdouble) pa_cvolume_get_fade (&map->priv->pa_volume, &map->priv->pa_map);
+        else
+                map->priv->extern_volume[FADE] = 0;
+        if (gvc_channel_map_has_lfe (map))
+                map->priv->extern_volume[LFE] = (gdouble) pa_cvolume_get_position (&map->priv->pa_volume, &map->priv->pa_map, PA_CHANNEL_POSITION_LFE);
+        else
+                map->priv->extern_volume[LFE] = 0;
+
+        return map->priv->extern_volume;
+}
+
+gboolean
+gvc_channel_map_can_balance (const GvcChannelMap  *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE);
+
+        return map->priv->can_balance;
+}
+
+gboolean
+gvc_channel_map_can_fade (const GvcChannelMap  *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE);
+
+        return map->priv->can_fade;
+}
+
+const char *
+gvc_channel_map_get_mapping (const GvcChannelMap  *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+        if (!pa_channel_map_valid(&map->priv->pa_map))
+                return NULL;
+
+        return pa_channel_map_to_pretty_name (&map->priv->pa_map);
+}
+
+/**
+ * gvc_channel_map_has_position: (skip)
+ * @map:
+ * @position:
+ *
+ * Returns:
+ */
+gboolean
+gvc_channel_map_has_position (const GvcChannelMap  *map,
+                              pa_channel_position_t position)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE);
+
+        return pa_channel_map_has_position (&(map->priv->pa_map), position);
+}
+
+const pa_channel_map *
+gvc_channel_map_get_pa_channel_map (const GvcChannelMap  *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+        if (!pa_channel_map_valid(&map->priv->pa_map))
+                return NULL;
+
+        return &map->priv->pa_map;
+}
+
+const pa_cvolume *
+gvc_channel_map_get_cvolume (const GvcChannelMap  *map)
+{
+        g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+        if (!pa_channel_map_valid(&map->priv->pa_map))
+                return NULL;
+
+        return &map->priv->pa_volume;
+}
+
+static void
+gvc_channel_map_class_init (GvcChannelMapClass *klass)
+{
+        GObjectClass   *gobject_class = G_OBJECT_CLASS (klass);
+
+        gobject_class->finalize = gvc_channel_map_finalize;
+
+        signals [VOLUME_CHANGED] =
+                g_signal_new ("volume-changed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcChannelMapClass, volume_changed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+void
+gvc_channel_map_volume_changed (GvcChannelMap     *map,
+                                const pa_cvolume  *cv,
+                                gboolean           set)
+{
+        g_return_if_fail (GVC_IS_CHANNEL_MAP (map));
+        g_return_if_fail (cv != NULL);
+        g_return_if_fail (pa_cvolume_compatible_with_channel_map(cv, &map->priv->pa_map));
+
+        if (pa_cvolume_equal(cv, &map->priv->pa_volume))
+                return;
+
+        map->priv->pa_volume = *cv;
+
+        if (map->priv->pa_volume_is_set == FALSE) {
+                map->priv->pa_volume_is_set = TRUE;
+                return;
+        }
+        g_signal_emit (map, signals[VOLUME_CHANGED], 0, set);
+}
+
+static void
+gvc_channel_map_init (GvcChannelMap *map)
+{
+        map->priv = gvc_channel_map_get_instance_private (map);
+        map->priv->pa_volume_is_set = FALSE;
+}
+
+static void
+gvc_channel_map_finalize (GObject *object)
+{
+        GvcChannelMap *channel_map;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_CHANNEL_MAP (object));
+
+        channel_map = GVC_CHANNEL_MAP (object);
+
+        g_return_if_fail (channel_map->priv != NULL);
+
+        G_OBJECT_CLASS (gvc_channel_map_parent_class)->finalize (object);
+}
+
+GvcChannelMap *
+gvc_channel_map_new (void)
+{
+        GObject *map;
+        map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL);
+        return GVC_CHANNEL_MAP (map);
+}
+
+static void
+set_from_pa_map (GvcChannelMap        *map,
+                 const pa_channel_map *pa_map)
+{
+        g_assert (pa_channel_map_valid(pa_map));
+
+        map->priv->can_balance = pa_channel_map_can_balance (pa_map);
+        map->priv->can_fade = pa_channel_map_can_fade (pa_map);
+
+        map->priv->pa_map = *pa_map;
+        pa_cvolume_set(&map->priv->pa_volume, pa_map->channels, PA_VOLUME_NORM);
+}
+
+GvcChannelMap *
+gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *pa_map)
+{
+        GObject *map;
+        map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL);
+
+        set_from_pa_map (GVC_CHANNEL_MAP (map), pa_map);
+
+        return GVC_CHANNEL_MAP (map);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-channel-map.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_CHANNEL_MAP_H
+#define __GVC_CHANNEL_MAP_H
+
+#include <glib-object.h>
+#include <gvc-pulseaudio-fake.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_CHANNEL_MAP         (gvc_channel_map_get_type ())
+#define GVC_CHANNEL_MAP(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMap))
+#define GVC_CHANNEL_MAP_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass))
+#define GVC_IS_CHANNEL_MAP(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_CHANNEL_MAP))
+#define GVC_IS_CHANNEL_MAP_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_CHANNEL_MAP))
+#define GVC_CHANNEL_MAP_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass))
+
+typedef struct GvcChannelMapPrivate GvcChannelMapPrivate;
+
+typedef struct
+{
+        GObject               parent;
+        GvcChannelMapPrivate *priv;
+} GvcChannelMap;
+
+typedef struct
+{
+        GObjectClass           parent_class;
+        void (*volume_changed) (GvcChannelMap *channel_map, gboolean set);
+} GvcChannelMapClass;
+
+enum {
+        VOLUME,
+        BALANCE,
+        FADE,
+        LFE,
+        NUM_TYPES
+};
+
+GType                   gvc_channel_map_get_type                (void);
+
+GvcChannelMap *         gvc_channel_map_new                     (void);
+guint                   gvc_channel_map_get_num_channels        (const GvcChannelMap  *map);
+const gdouble *         gvc_channel_map_get_volume              (GvcChannelMap  *map);
+gboolean                gvc_channel_map_can_balance             (const GvcChannelMap  *map);
+gboolean                gvc_channel_map_can_fade                (const GvcChannelMap  *map);
+gboolean                gvc_channel_map_has_position            (const GvcChannelMap  *map,
+                                                                 pa_channel_position_t position);
+#define                 gvc_channel_map_has_lfe(x)              gvc_channel_map_has_position (x, PA_CHANNEL_POSITION_LFE)
+
+const char *            gvc_channel_map_get_mapping             (const GvcChannelMap  *map);
+
+G_END_DECLS
+
+#endif /* __GVC_CHANNEL_MAP_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-card-private.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008-2009 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CARD_PRIVATE_H
+#define __GVC_MIXER_CARD_PRIVATE_H
+
+#include <pulse/pulseaudio.h>
+#include "gvc-mixer-card.h"
+
+G_BEGIN_DECLS
+
+GvcMixerCard *        gvc_mixer_card_new               (pa_context   *context,
+                                                        guint         index);
+pa_context *          gvc_mixer_card_get_pa_context    (GvcMixerCard *card);
+
+void gvc_mixer_card_add_port (GvcMixerCard     *card,
+                              GvcMixerCardPort *port);
+
+void gvc_mixer_card_remove_port (GvcMixerCard     *card,
+                                 GvcMixerCardPort *port);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CARD_PRIVATE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-card.c
@@ -0,0 +1,592 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ * Copyright (C) 2009 Bastien Nocera
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-card.h"
+#include "gvc-mixer-card-private.h"
+
+static guint32 card_serial = 1;
+
+struct GvcMixerCardPrivate
+{
+        pa_context    *pa_context;
+        guint          id;
+        guint          index;
+        char          *name;
+        char          *icon_name;
+        char          *profile;
+        char          *target_profile;
+        char          *human_profile;
+        GList         *profiles;
+        pa_operation  *profile_op;
+        GList         *ports;
+};
+
+enum
+{
+        PROP_0,
+        PROP_ID,
+        PROP_PA_CONTEXT,
+        PROP_INDEX,
+        PROP_NAME,
+        PROP_ICON_NAME,
+        PROP_PROFILE,
+        PROP_HUMAN_PROFILE,
+        N_PROPS
+};
+static GParamSpec *obj_props[N_PROPS] = { NULL, };
+
+static void     gvc_mixer_card_finalize   (GObject            *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerCard, gvc_mixer_card, G_TYPE_OBJECT)
+
+static guint32
+get_next_card_serial (void)
+{
+        guint32 serial;
+
+        serial = card_serial++;
+
+        if ((gint32)card_serial < 0) {
+                card_serial = 1;
+        }
+
+        return serial;
+}
+
+pa_context *
+gvc_mixer_card_get_pa_context (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0);
+        return card->priv->pa_context;
+}
+
+guint
+gvc_mixer_card_get_index (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0);
+        return card->priv->index;
+}
+
+guint
+gvc_mixer_card_get_id (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0);
+        return card->priv->id;
+}
+
+const char *
+gvc_mixer_card_get_name (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+        return card->priv->name;
+}
+
+gboolean
+gvc_mixer_card_set_name (GvcMixerCard *card,
+                         const char     *name)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+
+        g_free (card->priv->name);
+        card->priv->name = g_strdup (name);
+        g_object_notify_by_pspec (G_OBJECT (card), obj_props[PROP_NAME]);
+
+        return TRUE;
+}
+
+const char *
+gvc_mixer_card_get_icon_name (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+        return card->priv->icon_name;
+}
+
+gboolean
+gvc_mixer_card_set_icon_name (GvcMixerCard *card,
+                              const char     *icon_name)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+
+        g_free (card->priv->icon_name);
+        card->priv->icon_name = g_strdup (icon_name);
+        g_object_notify_by_pspec (G_OBJECT (card), obj_props[PROP_ICON_NAME]);
+
+        return TRUE;
+}
+
+/**
+ * gvc_mixer_card_get_profile: (skip)
+ * @card:
+ *
+ * Returns:
+ */
+GvcMixerCardProfile *
+gvc_mixer_card_get_profile (GvcMixerCard *card)
+{
+        GList *l;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+        g_return_val_if_fail (card->priv->profiles != NULL, NULL);
+
+        for (l = card->priv->profiles; l != NULL; l = l->next) {
+                GvcMixerCardProfile *p = l->data;
+                if (g_str_equal (card->priv->profile, p->profile)) {
+                        return p;
+                }
+        }
+
+        g_assert_not_reached ();
+
+        return NULL;
+}
+
+gboolean
+gvc_mixer_card_set_profile (GvcMixerCard *card,
+                            const char     *profile)
+{
+        GList *l;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+        g_return_val_if_fail (card->priv->profiles != NULL, FALSE);
+
+        if (g_strcmp0 (card->priv->profile, profile) == 0)
+                return TRUE;
+
+        g_free (card->priv->profile);
+        card->priv->profile = g_strdup (profile);
+
+        g_free (card->priv->human_profile);
+        card->priv->human_profile = NULL;
+
+        for (l = card->priv->profiles; l != NULL; l = l->next) {
+                GvcMixerCardProfile *p = l->data;
+                if (g_str_equal (card->priv->profile, p->profile)) {
+                        card->priv->human_profile = g_strdup (p->human_profile);
+                        break;
+                }
+        }
+
+        g_object_notify_by_pspec (G_OBJECT (card), obj_props[PROP_PROFILE]);
+
+        return TRUE;
+}
+
+static void
+_pa_context_set_card_profile_by_index_cb (pa_context                       *context,
+                                          int                               success,
+                                          void                             *userdata)
+{
+        GvcMixerCard *card = GVC_MIXER_CARD (userdata);
+
+        g_assert (card->priv->target_profile);
+
+        if (success > 0) {
+                gvc_mixer_card_set_profile (card, card->priv->target_profile);
+        } else {
+                g_debug ("Failed to switch profile on '%s' from '%s' to '%s'",
+                         card->priv->name,
+                         card->priv->profile,
+                         card->priv->target_profile);
+        }
+        g_free (card->priv->target_profile);
+        card->priv->target_profile = NULL;
+
+        pa_operation_unref (card->priv->profile_op);
+        card->priv->profile_op = NULL;
+}
+
+/**
+ * gvc_mixer_card_change_profile:
+ * @card: a #GvcMixerCard
+ * @profile: (allow-none): the profile to change to or %NULL.
+ *
+ * Change the profile in use on this card.
+ *
+ * Returns: %TRUE if profile successfully changed or already using this profile.
+ */
+gboolean
+gvc_mixer_card_change_profile (GvcMixerCard *card,
+                               const char *profile)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+        g_return_val_if_fail (card->priv->profiles != NULL, FALSE);
+
+        /* Same profile, or already requested? */
+        if (g_strcmp0 (card->priv->profile, profile) == 0)
+                return TRUE;
+        if (g_strcmp0 (profile, card->priv->target_profile) == 0)
+                return TRUE;
+        if (card->priv->profile_op != NULL) {
+                pa_operation_cancel (card->priv->profile_op);
+                pa_operation_unref (card->priv->profile_op);
+                card->priv->profile_op = NULL;
+        }
+
+        if (card->priv->profile != NULL) {
+                g_free (card->priv->target_profile);
+                card->priv->target_profile = g_strdup (profile);
+
+                card->priv->profile_op = pa_context_set_card_profile_by_index (card->priv->pa_context,
+                                                                               card->priv->index,
+                                                                               card->priv->target_profile,
+                                                                               _pa_context_set_card_profile_by_index_cb,
+                                                                               card);
+
+                if (card->priv->profile_op == NULL) {
+                        g_warning ("pa_context_set_card_profile_by_index() failed");
+                        return FALSE;
+                }
+        } else {
+                g_assert (card->priv->human_profile == NULL);
+                card->priv->profile = g_strdup (profile);
+        }
+
+        return TRUE;
+}
+
+/**
+ * gvc_mixer_card_get_profiles:
+ *
+ * Return value: (transfer none) (element-type GvcMixerCardProfile):
+ */
+const GList *
+gvc_mixer_card_get_profiles (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+        return card->priv->profiles;
+}
+
+/**
+ * gvc_mixer_card_get_ports:
+ *
+ * Return value: (transfer none) (element-type GvcMixerCardPort):
+ */
+const GList *
+gvc_mixer_card_get_ports (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+        return card->priv->ports;
+}
+
+/**
+ * gvc_mixer_card_profile_compare:
+ *
+ * Return value: 1 if @a has a higher priority, -1 if @b has a higher
+ * priority, 0 if @a and @b have the same priority.
+ */
+int
+gvc_mixer_card_profile_compare (GvcMixerCardProfile *a,
+                                GvcMixerCardProfile *b)
+{
+        if (a->priority == b->priority)
+                return 0;
+        if (a->priority > b->priority)
+                return 1;
+        return -1;
+}
+
+static void
+free_profile (GvcMixerCardProfile *p)
+{
+        g_free (p->profile);
+        g_free (p->human_profile);
+        g_free (p->status);
+        g_free (p);
+}
+
+/**
+ * gvc_mixer_card_set_profiles:
+ * @profiles: (transfer full) (element-type GvcMixerCardProfile):
+ */
+gboolean
+gvc_mixer_card_set_profiles (GvcMixerCard *card,
+                             GList        *profiles)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+
+        g_list_free_full (card->priv->profiles, (GDestroyNotify) free_profile);
+        card->priv->profiles = g_list_sort (profiles, (GCompareFunc) gvc_mixer_card_profile_compare);
+
+        return TRUE;
+}
+
+/**
+ * gvc_mixer_card_get_gicon:
+ * @card:
+ *
+ * Return value: (transfer full):
+ */
+GIcon *
+gvc_mixer_card_get_gicon (GvcMixerCard *card)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+
+        if (card->priv->icon_name == NULL)
+                return NULL;
+
+        return g_themed_icon_new_with_default_fallbacks (card->priv->icon_name);
+}
+
+static void
+free_port (GvcMixerCardPort *port)
+{
+        g_free (port->port);
+        g_free (port->human_port);
+        g_free (port->icon_name);
+        g_list_free (port->profiles);
+
+        g_free (port);
+}
+
+/**
+ * gvc_mixer_card_set_ports:
+ * @ports: (transfer full) (element-type GvcMixerCardPort):
+ */
+gboolean
+gvc_mixer_card_set_ports (GvcMixerCard *card,
+                          GList        *ports)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+        g_return_val_if_fail (card->priv->ports == NULL, FALSE);
+
+        g_list_free_full (card->priv->ports, (GDestroyNotify) free_port);
+        card->priv->ports = ports;
+
+        return TRUE;
+}
+
+void
+gvc_mixer_card_add_port (GvcMixerCard     *card,
+                         GvcMixerCardPort *port)
+{
+        card->priv->ports = g_list_prepend (card->priv->ports, port);
+}
+
+void
+gvc_mixer_card_remove_port (GvcMixerCard     *card,
+                            GvcMixerCardPort *port)
+{
+        card->priv->ports = g_list_remove (card->priv->ports, port);
+        free_port (port);
+}
+
+static void
+gvc_mixer_card_set_property (GObject       *object,
+                             guint          prop_id,
+                             const GValue  *value,
+                             GParamSpec    *pspec)
+{
+        GvcMixerCard *self = GVC_MIXER_CARD (object);
+
+        switch (prop_id) {
+        case PROP_PA_CONTEXT:
+                self->priv->pa_context = g_value_get_pointer (value);
+                break;
+        case PROP_INDEX:
+                self->priv->index = g_value_get_ulong (value);
+                break;
+        case PROP_ID:
+                self->priv->id = g_value_get_ulong (value);
+                break;
+        case PROP_NAME:
+                gvc_mixer_card_set_name (self, g_value_get_string (value));
+                break;
+        case PROP_ICON_NAME:
+                gvc_mixer_card_set_icon_name (self, g_value_get_string (value));
+                break;
+        case PROP_PROFILE:
+                gvc_mixer_card_set_profile (self, g_value_get_string (value));
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gvc_mixer_card_get_property (GObject     *object,
+                             guint        prop_id,
+                             GValue      *value,
+                             GParamSpec  *pspec)
+{
+        GvcMixerCard *self = GVC_MIXER_CARD (object);
+
+        switch (prop_id) {
+        case PROP_PA_CONTEXT:
+                g_value_set_pointer (value, self->priv->pa_context);
+                break;
+        case PROP_INDEX:
+                g_value_set_ulong (value, self->priv->index);
+                break;
+        case PROP_ID:
+                g_value_set_ulong (value, self->priv->id);
+                break;
+        case PROP_NAME:
+                g_value_set_string (value, self->priv->name);
+                break;
+        case PROP_ICON_NAME:
+                g_value_set_string (value, self->priv->icon_name);
+                break;
+        case PROP_PROFILE:
+                g_value_set_string (value, self->priv->profile);
+                break;
+        case PROP_HUMAN_PROFILE:
+                g_value_set_string (value, self->priv->human_profile);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static GObject *
+gvc_mixer_card_constructor (GType                  type,
+                            guint                  n_construct_properties,
+                            GObjectConstructParam *construct_params)
+{
+        GObject       *object;
+        GvcMixerCard *self;
+
+        object = G_OBJECT_CLASS (gvc_mixer_card_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+        self = GVC_MIXER_CARD (object);
+
+        self->priv->id = get_next_card_serial ();
+
+        return object;
+}
+
+static void
+gvc_mixer_card_class_init (GvcMixerCardClass *klass)
+{
+        GObjectClass   *gobject_class = G_OBJECT_CLASS (klass);
+
+        gobject_class->constructor = gvc_mixer_card_constructor;
+        gobject_class->finalize = gvc_mixer_card_finalize;
+
+        gobject_class->set_property = gvc_mixer_card_set_property;
+        gobject_class->get_property = gvc_mixer_card_get_property;
+
+        obj_props[PROP_INDEX] = g_param_spec_ulong ("index",
+                                                    "Index",
+                                                    "The index for this card",
+                                                    0, G_MAXULONG, 0,
+                                                    G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_ID] = g_param_spec_ulong ("id",
+                                                 "id",
+                                                 "The id for this card",
+                                                 0, G_MAXULONG, 0,
+                                                 G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_PA_CONTEXT] = g_param_spec_pointer ("pa-context",
+                                                           "PulseAudio context",
+                                                           "The PulseAudio context for this card",
+                                                           G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_NAME] = g_param_spec_string ("name",
+                                                    "Name",
+                                                    "Name to display for this card",
+                                                    NULL,
+                                                    G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_ICON_NAME] = g_param_spec_string ("icon-name",
+                                                         "Icon Name",
+                                                         "Name of icon to display for this card",
+                                                         NULL,
+                                                         G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_PROFILE] = g_param_spec_string ("profile",
+                                                       "Profile",
+                                                       "Name of current profile for this card",
+                                                       NULL,
+                                                       G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_HUMAN_PROFILE] = g_param_spec_string ("human-profile",
+                                                             "Profile (Human readable)",
+                                                             "Name of current profile for this card in human readable form",
+                                                             NULL,
+                                                             G_PARAM_READABLE|G_PARAM_STATIC_STRINGS);
+
+        g_object_class_install_properties (gobject_class, N_PROPS, obj_props);
+}
+
+static void
+gvc_mixer_card_init (GvcMixerCard *card)
+{
+        card->priv = gvc_mixer_card_get_instance_private (card);
+}
+
+GvcMixerCard *
+gvc_mixer_card_new (pa_context *context,
+                    guint       index)
+{
+        GObject *object;
+
+        object = g_object_new (GVC_TYPE_MIXER_CARD,
+                               "index", index,
+                               "pa-context", context,
+                               NULL);
+        return GVC_MIXER_CARD (object);
+}
+
+static void
+gvc_mixer_card_finalize (GObject *object)
+{
+        GvcMixerCard *mixer_card;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_CARD (object));
+
+        mixer_card = GVC_MIXER_CARD (object);
+
+        g_return_if_fail (mixer_card->priv != NULL);
+
+        g_free (mixer_card->priv->name);
+        mixer_card->priv->name = NULL;
+
+        g_free (mixer_card->priv->icon_name);
+        mixer_card->priv->icon_name = NULL;
+
+        g_free (mixer_card->priv->target_profile);
+        mixer_card->priv->target_profile = NULL;
+
+        g_free (mixer_card->priv->profile);
+        mixer_card->priv->profile = NULL;
+
+        g_free (mixer_card->priv->human_profile);
+        mixer_card->priv->human_profile = NULL;
+
+        g_list_free_full (mixer_card->priv->profiles, (GDestroyNotify) free_profile);
+        mixer_card->priv->profiles = NULL;
+
+        g_list_free_full (mixer_card->priv->ports, (GDestroyNotify) free_port);
+        mixer_card->priv->ports = NULL;
+
+        G_OBJECT_CLASS (gvc_mixer_card_parent_class)->finalize (object);
+}
+
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-card.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008-2009 Red Hat, Inc.
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CARD_H
+#define __GVC_MIXER_CARD_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_CARD         (gvc_mixer_card_get_type ())
+#define GVC_MIXER_CARD(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CARD, GvcMixerCard))
+#define GVC_MIXER_CARD_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CARD, GvcMixerCardClass))
+#define GVC_IS_MIXER_CARD(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CARD))
+#define GVC_IS_MIXER_CARD_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CARD))
+#define GVC_MIXER_CARD_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CARD, GvcMixerCardClass))
+
+typedef struct GvcMixerCardPrivate GvcMixerCardPrivate;
+
+typedef struct
+{
+        GObject                parent;
+        GvcMixerCardPrivate   *priv;
+} GvcMixerCard;
+
+typedef struct
+{
+        GObjectClass           parent_class;
+
+        /* vtable */
+} GvcMixerCardClass;
+
+typedef struct
+{
+        char  *profile;
+        char  *human_profile;
+        char  *status;
+        guint  priority;
+        guint  n_sinks, n_sources;
+} GvcMixerCardProfile;
+
+typedef struct
+{
+        char  *port;
+        char  *human_port;
+        char  *icon_name;
+        guint  priority;
+        gint   available;
+        gint   direction;
+        GList *profiles;
+} GvcMixerCardPort;
+
+GType                 gvc_mixer_card_get_type          (void);
+
+guint                 gvc_mixer_card_get_id            (GvcMixerCard *card);
+guint                 gvc_mixer_card_get_index         (GvcMixerCard *card);
+const char *          gvc_mixer_card_get_name          (GvcMixerCard *card);
+const char *          gvc_mixer_card_get_icon_name     (GvcMixerCard *card);
+GvcMixerCardProfile * gvc_mixer_card_get_profile       (GvcMixerCard *card);
+const GList *         gvc_mixer_card_get_profiles      (GvcMixerCard *card);
+const GList *         gvc_mixer_card_get_ports         (GvcMixerCard *card);
+gboolean              gvc_mixer_card_change_profile    (GvcMixerCard *card,
+                                                        const char *profile);
+GIcon *               gvc_mixer_card_get_gicon         (GvcMixerCard *card);
+
+int                   gvc_mixer_card_profile_compare   (GvcMixerCardProfile *a,
+                                                        GvcMixerCardProfile *b);
+
+/* private */
+gboolean              gvc_mixer_card_set_name          (GvcMixerCard *card,
+                                                        const char   *name);
+gboolean              gvc_mixer_card_set_icon_name     (GvcMixerCard *card,
+                                                        const char   *name);
+gboolean              gvc_mixer_card_set_profile       (GvcMixerCard *card,
+                                                        const char   *profile);
+gboolean              gvc_mixer_card_set_profiles      (GvcMixerCard *card,
+                                                        GList        *profiles);
+gboolean              gvc_mixer_card_set_ports         (GvcMixerCard *stream,
+                                                        GList        *ports);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CARD_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-control-private.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CONTROL_PRIVATE_H
+#define __GVC_MIXER_CONTROL_PRIVATE_H
+
+#include <glib-object.h>
+#include <pulse/pulseaudio.h>
+#include "gvc-mixer-stream.h"
+#include "gvc-mixer-card.h"
+
+G_BEGIN_DECLS
+
+pa_context *        gvc_mixer_control_get_pa_context      (GvcMixerControl *control);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CONTROL_PRIVATE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-control.c
@@ -0,0 +1,3877 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2006-2008 Lennart Poettering
+ * Copyright (C) 2008 Sjoerd Simons <sjoerd@luon.net>
+ * Copyright (C) 2008 William Jon McCann
+ * Copyright (C) 2012 Conor Curran
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/glib-mainloop.h>
+#include <pulse/ext-stream-restore.h>
+
+#ifdef HAVE_ALSA
+#include <alsa/asoundlib.h>
+#endif /* HAVE_ALSA */
+
+#include "gvc-mixer-control.h"
+#include "gvc-mixer-sink.h"
+#include "gvc-mixer-source.h"
+#include "gvc-mixer-sink-input.h"
+#include "gvc-mixer-source-output.h"
+#include "gvc-mixer-event-role.h"
+#include "gvc-mixer-card.h"
+#include "gvc-mixer-card-private.h"
+#include "gvc-channel-map-private.h"
+#include "gvc-mixer-control-private.h"
+#include "gvc-mixer-ui-device.h"
+
+#define RECONNECT_DELAY 5
+
+enum {
+        PROP_0,
+        PROP_NAME,
+        N_PROPS
+};
+static GParamSpec *obj_props[N_PROPS] = { NULL, };
+
+struct GvcMixerControlPrivate
+{
+        pa_glib_mainloop *pa_mainloop;
+        pa_mainloop_api  *pa_api;
+        pa_context       *pa_context;
+        guint             server_protocol_version;
+        int               n_outstanding;
+        guint             reconnect_id;
+        char             *name;
+
+        gboolean          default_sink_is_set;
+        guint             default_sink_id;
+        char             *default_sink_name;
+        gboolean          default_source_is_set;
+        guint             default_source_id;
+        char             *default_source_name;
+
+        gboolean          event_sink_input_is_set;
+        guint             event_sink_input_id;
+
+        GHashTable       *all_streams;
+        GHashTable       *sinks; /* fixed outputs */
+        GHashTable       *sources; /* fixed inputs */
+        GHashTable       *sink_inputs; /* routable output streams */
+        GHashTable       *source_outputs; /* routable input streams */
+        GHashTable       *clients;
+        GHashTable       *cards;
+
+        GvcMixerStream   *new_default_sink_stream; /* new default sink stream, used in gvc_mixer_control_set_default_sink () */
+        GvcMixerStream   *new_default_source_stream; /* new default source stream, used in gvc_mixer_control_set_default_source () */
+
+        GHashTable       *ui_outputs; /* UI visible outputs */
+        GHashTable       *ui_inputs;  /* UI visible inputs */
+
+        /* When we change profile on a device that is not the server default sink,
+         * it will jump back to the default sink set by the server to prevent the
+         * audio setup from being 'outputless'.
+         *
+         * All well and good but then when we get the new stream created for the
+         * new profile how do we know that this is the intended default or selected
+         * device the user wishes to use. */
+        guint            profile_swapping_device_id;
+
+#ifdef HAVE_ALSA
+        int      headset_card;
+        gboolean has_headsetmic;
+        gboolean has_headphonemic;
+        gboolean headset_plugged_in;
+        char    *headphones_name;
+        char    *headsetmic_name;
+        char    *headphonemic_name;
+        char    *internalspk_name;
+        char    *internalmic_name;
+#endif /* HAVE_ALSA */
+
+        GvcMixerControlState state;
+};
+
+enum {
+        STATE_CHANGED,
+        STREAM_ADDED,
+        STREAM_REMOVED,
+        STREAM_CHANGED,
+        CARD_ADDED,
+        CARD_REMOVED,
+        DEFAULT_SINK_CHANGED,
+        DEFAULT_SOURCE_CHANGED,
+        ACTIVE_OUTPUT_UPDATE,
+        ACTIVE_INPUT_UPDATE,
+        OUTPUT_ADDED,
+        INPUT_ADDED,
+        OUTPUT_REMOVED,
+        INPUT_REMOVED,
+        AUDIO_DEVICE_SELECTION_NEEDED,
+        LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL] = { 0, };
+
+static void     gvc_mixer_control_finalize   (GObject              *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT)
+
+pa_context *
+gvc_mixer_control_get_pa_context (GvcMixerControl *control)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+        return control->priv->pa_context;
+}
+
+/**
+ * gvc_mixer_control_get_event_sink_input:
+ * @control:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_event_sink_input (GvcMixerControl *control)
+{
+        GvcMixerStream *stream;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        stream = g_hash_table_lookup (control->priv->all_streams,
+                                      GUINT_TO_POINTER (control->priv->event_sink_input_id));
+
+        return stream;
+}
+
+static void
+gvc_mixer_control_stream_restore_cb (pa_context *c,
+				     GvcMixerStream *new_stream,
+                                     const pa_ext_stream_restore_info *info,
+                                     GvcMixerControl *control)
+{
+        pa_operation *o;
+        pa_ext_stream_restore_info new_info;
+
+        if (new_stream == NULL)
+                return;
+
+        new_info.name = info->name;
+        new_info.channel_map = info->channel_map;
+        new_info.volume = info->volume;
+        new_info.mute = info->mute;
+
+        new_info.device = gvc_mixer_stream_get_name (new_stream);
+
+        o = pa_ext_stream_restore_write (control->priv->pa_context,
+                                         PA_UPDATE_REPLACE,
+                                         &new_info, 1,
+                                         TRUE, NULL, NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_ext_stream_restore_write() failed: %s",
+                           pa_strerror (pa_context_errno (control->priv->pa_context)));
+                return;
+        }
+
+        g_debug ("Changed default device for %s to %s", info->name, new_info.device);
+
+        pa_operation_unref (o);
+}
+
+static void
+gvc_mixer_control_stream_restore_sink_cb (pa_context *c,
+                                          const pa_ext_stream_restore_info *info,
+                                          int eol,
+                                          void *userdata)
+{
+        GvcMixerControl *control = (GvcMixerControl *) userdata;
+        if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by"))
+                return;
+        gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control);
+}
+
+static void
+gvc_mixer_control_stream_restore_source_cb (pa_context *c,
+                                            const pa_ext_stream_restore_info *info,
+                                            int eol,
+                                            void *userdata)
+{
+        GvcMixerControl *control = (GvcMixerControl *) userdata;
+        if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by"))
+                return;
+        gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control);
+}
+
+/**
+ * gvc_mixer_control_lookup_device_from_stream:
+ * @control:
+ * @stream:
+ *
+ * Returns: (transfer none): a #GvcUIDevice or %NULL
+ */
+GvcMixerUIDevice *
+gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control,
+                                             GvcMixerStream *stream)
+{
+        GList                   *devices, *d;
+        gboolean                 is_network_stream;
+        const GList             *ports;
+        GvcMixerUIDevice        *ret;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+
+        if (GVC_IS_MIXER_SOURCE (stream))
+               devices = g_hash_table_get_values (control->priv->ui_inputs);
+        else
+               devices = g_hash_table_get_values (control->priv->ui_outputs);
+
+        ret = NULL;
+        ports = gvc_mixer_stream_get_ports (stream);
+        is_network_stream = (ports == NULL);
+
+        for (d = devices; d != NULL; d = d->next) {
+                GvcMixerUIDevice *device = d->data;
+                guint stream_id = G_MAXUINT;
+
+                g_object_get (G_OBJECT (device),
+                             "stream-id", &stream_id,
+                              NULL);
+
+                if (is_network_stream &&
+                    stream_id == gvc_mixer_stream_get_id (stream)) {
+                        g_debug ("lookup device from stream - %s - it is a network_stream ",
+                                 gvc_mixer_ui_device_get_description (device));
+                        ret = device;
+                        break;
+                } else if (!is_network_stream) {
+                        const GvcMixerStreamPort *port;
+                        port = gvc_mixer_stream_get_port (stream);
+
+                        if (stream_id == gvc_mixer_stream_get_id (stream) &&
+                            g_strcmp0 (gvc_mixer_ui_device_get_port (device),
+                                       port->port) == 0) {
+                                g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'",
+                                         gvc_mixer_ui_device_get_description (device),
+                                         gvc_mixer_ui_device_get_port (device),
+                                         stream_id,
+                                         port->port,
+                                         gvc_mixer_stream_get_id (stream),
+                                         gvc_mixer_stream_get_description (stream));
+                                ret = device;
+                                break;
+                        }
+                }
+        }
+
+        g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream));
+
+        g_list_free (devices);
+
+        return ret;
+}
+
+gboolean
+gvc_mixer_control_set_default_sink (GvcMixerControl *control,
+                                    GvcMixerStream  *stream)
+{
+        pa_operation *o;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_debug ("about to set default sink on server");
+        o = pa_context_set_default_sink (control->priv->pa_context,
+                                         gvc_mixer_stream_get_name (stream),
+                                         NULL,
+                                         NULL);
+        if (o == NULL) {
+                g_warning ("pa_context_set_default_sink() failed: %s",
+                           pa_strerror (pa_context_errno (control->priv->pa_context)));
+                return FALSE;
+        }
+
+        pa_operation_unref (o);
+
+        control->priv->new_default_sink_stream = stream;
+        g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream);
+
+        o = pa_ext_stream_restore_read (control->priv->pa_context,
+                                        gvc_mixer_control_stream_restore_sink_cb,
+                                        control);
+
+        if (o == NULL) {
+                g_warning ("pa_ext_stream_restore_read() failed: %s",
+                           pa_strerror (pa_context_errno (control->priv->pa_context)));
+                return FALSE;
+        }
+
+        pa_operation_unref (o);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_control_set_default_source (GvcMixerControl *control,
+                                      GvcMixerStream  *stream)
+{
+        GvcMixerUIDevice* input;
+        pa_operation *o;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        o = pa_context_set_default_source (control->priv->pa_context,
+                                           gvc_mixer_stream_get_name (stream),
+                                           NULL,
+                                           NULL);
+        if (o == NULL) {
+                g_warning ("pa_context_set_default_source() failed");
+                return FALSE;
+        }
+
+        pa_operation_unref (o);
+
+        control->priv->new_default_source_stream = stream;
+        g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream);
+
+        o = pa_ext_stream_restore_read (control->priv->pa_context,
+                                        gvc_mixer_control_stream_restore_source_cb,
+                                        control);
+
+        if (o == NULL) {
+                g_warning ("pa_ext_stream_restore_read() failed: %s",
+                           pa_strerror (pa_context_errno (control->priv->pa_context)));
+                return FALSE;
+        }
+
+        pa_operation_unref (o);
+
+        /* source change successful, update the UI. */
+        input = gvc_mixer_control_lookup_device_from_stream (control, stream);
+        g_signal_emit (G_OBJECT (control),
+                       signals[ACTIVE_INPUT_UPDATE],
+                       0,
+                       gvc_mixer_ui_device_get_id (input));
+
+        return TRUE;
+}
+
+/**
+ * gvc_mixer_control_get_default_sink:
+ * @control:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_default_sink (GvcMixerControl *control)
+{
+        GvcMixerStream *stream;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        if (control->priv->default_sink_is_set) {
+                stream = g_hash_table_lookup (control->priv->all_streams,
+                                              GUINT_TO_POINTER (control->priv->default_sink_id));
+        } else {
+                stream = NULL;
+        }
+
+        return stream;
+}
+
+/**
+ * gvc_mixer_control_get_default_source:
+ * @control:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_default_source (GvcMixerControl *control)
+{
+        GvcMixerStream *stream;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        if (control->priv->default_source_is_set) {
+                stream = g_hash_table_lookup (control->priv->all_streams,
+                                              GUINT_TO_POINTER (control->priv->default_source_id));
+        } else {
+                stream = NULL;
+        }
+
+        return stream;
+}
+
+static gpointer
+gvc_mixer_control_lookup_id (GHashTable *hash_table,
+                             guint       id)
+{
+        return g_hash_table_lookup (hash_table,
+                                    GUINT_TO_POINTER (id));
+}
+
+/**
+ * gvc_mixer_control_lookup_stream_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_lookup_stream_id (GvcMixerControl *control,
+                                    guint            id)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        return gvc_mixer_control_lookup_id (control->priv->all_streams, id);
+}
+
+/**
+ * gvc_mixer_control_lookup_card_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerCard *
+gvc_mixer_control_lookup_card_id (GvcMixerControl *control,
+                                  guint            id)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        return gvc_mixer_control_lookup_id (control->priv->cards, id);
+}
+
+/**
+ * gvc_mixer_control_lookup_output_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerUIDevice *
+gvc_mixer_control_lookup_output_id (GvcMixerControl *control,
+                                    guint            id)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id);
+}
+
+/**
+ * gvc_mixer_control_lookup_input_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerUIDevice *
+gvc_mixer_control_lookup_input_id (GvcMixerControl *control,
+                                    guint            id)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id);
+}
+
+/**
+ * gvc_mixer_control_get_stream_from_device:
+ * @control:
+ * @device:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_stream_from_device (GvcMixerControl *control,
+                                          GvcMixerUIDevice *device)
+{
+        gint stream_id;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        stream_id = gvc_mixer_ui_device_get_stream_id (device);
+
+        if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) {
+                g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream");
+                return NULL;
+        }
+        return gvc_mixer_control_lookup_stream_id (control, stream_id);
+}
+
+/**
+ * gvc_mixer_control_change_profile_on_selected_device:
+ * @control:
+ * @device:
+ * @profile: (allow-none): Can be %NULL if any profile present on this port is okay
+ *
+ * Returns: This method will attempt to swap the profile on the card of
+ * the device with given profile name.  If successfull it will set the
+ * preferred profile on that device so as we know the next time the user
+ * moves to that device it should have this profile active.
+ */
+gboolean
+gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl  *control,
+                                                     GvcMixerUIDevice *device,
+                                                     const gchar      *profile)
+{
+        const gchar         *best_profile;
+        GvcMixerCardProfile *current_profile;
+        GvcMixerCard        *card;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+        g_object_get (G_OBJECT (device), "card", &card, NULL);
+        current_profile = gvc_mixer_card_get_profile (card);
+
+        if (current_profile)
+                best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile);
+        else
+                best_profile = profile;
+
+        g_assert (best_profile);
+
+        g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i",
+                profile ? profile : "(any)", best_profile,
+                gvc_mixer_card_get_name (card),
+                gvc_mixer_ui_device_get_stream_id (device));
+
+        g_debug ("default sink name = %s and default sink id %u",
+                 control->priv->default_sink_name,
+                 control->priv->default_sink_id);
+
+        control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device);
+
+        if (gvc_mixer_card_change_profile (card, best_profile)) {
+                gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile);
+                return TRUE;
+        }
+        return FALSE;
+}
+
+/**
+ * gvc_mixer_control_change_output:
+ * @control:
+ * @output:
+ * This method is called from the UI when the user selects a previously unselected device.
+ * - Firstly it queries the stream from the device.
+ *   - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
+ *   In the scenario of a NULL stream on the device
+ *        - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
+ *        - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered
+ *          from when we attempt to change profile we will know exactly what device to highlight on that stream.
+ *        - It attempts to swap the profile on the card from that device and returns.
+ * - Next, it handles network or bluetooth streams that only require their stream to be made the default.
+ * - Next it deals with port changes so if the stream's active port is not the same as the port on the device
+ *   it will attempt to change the port on that stream to be same as the device. If this fails it will return.
+ * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device.
+ */
+void
+gvc_mixer_control_change_output (GvcMixerControl *control,
+                                 GvcMixerUIDevice* output)
+{
+        GvcMixerStream           *stream;
+        GvcMixerStream           *default_stream;
+        const GvcMixerStreamPort *active_port;
+        const gchar              *output_port;
+
+        g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
+        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output));
+
+        g_debug ("control change output");
+
+        stream = gvc_mixer_control_get_stream_from_device (control, output);
+        if (stream == NULL) {
+                gvc_mixer_control_change_profile_on_selected_device (control,
+                        output, NULL);
+                return;
+        }
+
+        if (!gvc_mixer_ui_device_has_ports (output)) {
+                g_debug ("Did we try to move to a software/bluetooth sink ?");
+                if (gvc_mixer_control_set_default_sink (control, stream)) {
+                        /* sink change was successful,  update the UI.*/
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[ACTIVE_OUTPUT_UPDATE],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (output));
+                }
+                else {
+                        g_warning ("Failed to set default sink with stream from output %s",
+                                   gvc_mixer_ui_device_get_description (output));
+                }
+                return;
+        }
+
+        active_port = gvc_mixer_stream_get_port (stream);
+        output_port = gvc_mixer_ui_device_get_port (output);
+        /* First ensure the correct port is active on the sink */
+        if (g_strcmp0 (active_port->port, output_port) != 0) {
+                g_debug ("Port change, switch to = %s", output_port);
+                if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) {
+                        g_warning ("Could not change port !");
+                        return;
+                }
+        }
+
+        default_stream = gvc_mixer_control_get_default_sink (control);
+
+        /* Finally if we are not on the correct stream, swap over. */
+        if (stream != default_stream) {
+                GvcMixerUIDevice* device;
+
+                g_debug ("Attempting to swap over to stream %s ",
+                         gvc_mixer_stream_get_description (stream));
+                if (gvc_mixer_control_set_default_sink (control, stream)) {
+                        device = gvc_mixer_control_lookup_device_from_stream (control, stream);
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[ACTIVE_OUTPUT_UPDATE],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (device));
+                } else {
+                        /* If the move failed for some reason reset the UI. */
+                        device = gvc_mixer_control_lookup_device_from_stream (control, default_stream);
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[ACTIVE_OUTPUT_UPDATE],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (device));
+                }
+        }
+}
+
+
+/**
+ * gvc_mixer_control_change_input:
+ * @control:
+ * @input:
+ * This method is called from the UI when the user selects a previously unselected device.
+ * - Firstly it queries the stream from the device.
+ *   - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
+ *   In the scenario of a NULL stream on the device
+ *        - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
+ *        - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered
+ *          from when we attempt to change profile we will know exactly what device to highlight on that stream.
+ *        - It attempts to swap the profile on the card from that device and returns.
+ * - Next, it handles network or bluetooth streams that only require their stream to be made the default.
+ * - Next it deals with port changes so if the stream's active port is not the same as the port on the device
+ *   it will attempt to change the port on that stream to be same as the device. If this fails it will return.
+ * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device.
+ */
+void
+gvc_mixer_control_change_input (GvcMixerControl *control,
+                                GvcMixerUIDevice* input)
+{
+        GvcMixerStream           *stream;
+        GvcMixerStream           *default_stream;
+        const GvcMixerStreamPort *active_port;
+        const gchar              *input_port;
+
+        g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
+        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input));
+
+        stream = gvc_mixer_control_get_stream_from_device (control, input);
+        if (stream == NULL) {
+                gvc_mixer_control_change_profile_on_selected_device (control,
+                        input, NULL);
+                return;
+        }
+
+        if (!gvc_mixer_ui_device_has_ports (input)) {
+                g_debug ("Did we try to move to a software/bluetooth source ?");
+                if (! gvc_mixer_control_set_default_source (control, stream)) {
+                        g_warning ("Failed to set default source with stream from input %s",
+                                   gvc_mixer_ui_device_get_description (input));
+                }
+                return;
+        }
+
+        active_port = gvc_mixer_stream_get_port (stream);
+        input_port = gvc_mixer_ui_device_get_port (input);
+        /* First ensure the correct port is active on the sink */
+        if (g_strcmp0 (active_port->port, input_port) != 0) {
+                g_debug ("Port change, switch to = %s", input_port);
+                if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) {
+                        g_warning ("Could not change port!");
+                        return;
+                }
+        }
+
+        default_stream = gvc_mixer_control_get_default_source (control);
+
+        /* Finally if we are not on the correct stream, swap over. */
+        if (stream != default_stream) {
+                g_debug ("change-input - attempting to swap over to stream %s",
+                         gvc_mixer_stream_get_description (stream));
+                gvc_mixer_control_set_default_source (control, stream);
+        }
+}
+
+
+static void
+listify_hash_values_hfunc (gpointer key,
+                           gpointer value,
+                           gpointer user_data)
+{
+        GSList **list = user_data;
+
+        *list = g_slist_prepend (*list, value);
+}
+
+static int
+gvc_name_collate (const char *namea,
+                  const char *nameb)
+{
+        if (nameb == NULL && namea == NULL)
+                return 0;
+        if (nameb == NULL)
+                return 1;
+        if (namea == NULL)
+                return -1;
+
+        return g_utf8_collate (namea, nameb);
+}
+
+static int
+gvc_card_collate (GvcMixerCard *a,
+                  GvcMixerCard *b)
+{
+        const char *namea;
+        const char *nameb;
+
+        g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0);
+        g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0);
+
+        namea = gvc_mixer_card_get_name (a);
+        nameb = gvc_mixer_card_get_name (b);
+
+        return gvc_name_collate (namea, nameb);
+}
+
+/**
+ * gvc_mixer_control_get_cards:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerCard):
+ */
+GSList *
+gvc_mixer_control_get_cards (GvcMixerControl *control)
+{
+        GSList *retval;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        retval = NULL;
+        g_hash_table_foreach (control->priv->cards,
+                              listify_hash_values_hfunc,
+                              &retval);
+        return g_slist_sort (retval, (GCompareFunc) gvc_card_collate);
+}
+
+static int
+gvc_stream_collate (GvcMixerStream *a,
+                    GvcMixerStream *b)
+{
+        const char *namea;
+        const char *nameb;
+
+        g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0);
+        g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0);
+
+        namea = gvc_mixer_stream_get_name (a);
+        nameb = gvc_mixer_stream_get_name (b);
+
+        return gvc_name_collate (namea, nameb);
+}
+
+/**
+ * gvc_mixer_control_get_streams:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerStream):
+ */
+GSList *
+gvc_mixer_control_get_streams (GvcMixerControl *control)
+{
+        GSList *retval;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        retval = NULL;
+        g_hash_table_foreach (control->priv->all_streams,
+                              listify_hash_values_hfunc,
+                              &retval);
+        return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_sinks:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSink):
+ */
+GSList *
+gvc_mixer_control_get_sinks (GvcMixerControl *control)
+{
+        GSList *retval;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        retval = NULL;
+        g_hash_table_foreach (control->priv->sinks,
+                              listify_hash_values_hfunc,
+                              &retval);
+        return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_sources:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSource):
+ */
+GSList *
+gvc_mixer_control_get_sources (GvcMixerControl *control)
+{
+        GSList *retval;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        retval = NULL;
+        g_hash_table_foreach (control->priv->sources,
+                              listify_hash_values_hfunc,
+                              &retval);
+        return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_sink_inputs:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSinkInput):
+ */
+GSList *
+gvc_mixer_control_get_sink_inputs (GvcMixerControl *control)
+{
+        GSList *retval;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        retval = NULL;
+        g_hash_table_foreach (control->priv->sink_inputs,
+                              listify_hash_values_hfunc,
+                              &retval);
+        return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_source_outputs:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSourceOutput):
+ */
+GSList *
+gvc_mixer_control_get_source_outputs (GvcMixerControl *control)
+{
+        GSList *retval;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+        retval = NULL;
+        g_hash_table_foreach (control->priv->source_outputs,
+                              listify_hash_values_hfunc,
+                              &retval);
+        return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+static void
+dec_outstanding (GvcMixerControl *control)
+{
+        if (control->priv->n_outstanding <= 0) {
+                return;
+        }
+
+        if (--control->priv->n_outstanding <= 0) {
+                control->priv->state = GVC_STATE_READY;
+                g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY);
+        }
+}
+
+GvcMixerControlState
+gvc_mixer_control_get_state (GvcMixerControl *control)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), GVC_STATE_CLOSED);
+
+        return control->priv->state;
+}
+
+static void
+on_default_source_port_notify (GObject        *object,
+                               GParamSpec     *pspec,
+                               GvcMixerControl *control)
+{
+        char             *port;
+        GvcMixerUIDevice *input;
+
+        g_object_get (object, "port", &port, NULL);
+        input = gvc_mixer_control_lookup_device_from_stream (control,
+                                                             GVC_MIXER_STREAM (object));
+
+        g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'",
+                 port,
+                 gvc_mixer_ui_device_get_description (input));
+
+        g_signal_emit (G_OBJECT (control),
+                       signals[ACTIVE_INPUT_UPDATE],
+                       0,
+                       gvc_mixer_ui_device_get_id (input));
+
+        g_free (port);
+}
+
+
+static void
+_set_default_source (GvcMixerControl *control,
+                     GvcMixerStream  *stream)
+{
+        guint new_id;
+
+        if (stream == NULL) {
+                if (!control->priv->default_source_is_set)
+                        return;
+
+                g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control),
+                                                      on_default_source_port_notify,
+                                                      control);
+
+                control->priv->default_source_id = 0;
+                control->priv->default_source_is_set = FALSE;
+                g_signal_emit (control,
+                               signals[DEFAULT_SOURCE_CHANGED],
+                               0,
+                               PA_INVALID_INDEX);
+                return;
+        }
+
+        new_id = gvc_mixer_stream_get_id (stream);
+
+        if (control->priv->default_source_id != new_id) {
+                GvcMixerUIDevice *input;
+
+                if (control->priv->default_source_is_set) {
+                        g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control),
+                                                              on_default_source_port_notify,
+                                                              control);
+                }
+
+                g_signal_connect (stream,
+                                  "notify::port",
+                                  G_CALLBACK (on_default_source_port_notify),
+                                  control);
+
+                control->priv->default_source_id = new_id;
+                control->priv->default_source_is_set = TRUE;
+                g_signal_emit (control,
+                               signals[DEFAULT_SOURCE_CHANGED],
+                               0,
+                               new_id);
+
+                input = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+                g_signal_emit (G_OBJECT (control),
+                               signals[ACTIVE_INPUT_UPDATE],
+                               0,
+                               gvc_mixer_ui_device_get_id (input));
+        }
+}
+
+static void
+on_default_sink_port_notify (GObject        *object,
+                             GParamSpec     *pspec,
+                             GvcMixerControl *control)
+{
+        char             *port;
+        GvcMixerUIDevice *output;
+
+        g_object_get (object, "port", &port, NULL);
+
+        output = gvc_mixer_control_lookup_device_from_stream (control,
+                                                              GVC_MIXER_STREAM (object));
+        if (output != NULL) {
+                g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s",
+                         port,
+                         gvc_mixer_ui_device_get_description (output));
+                g_signal_emit (G_OBJECT (control),
+                               signals[ACTIVE_OUTPUT_UPDATE],
+                               0,
+                               gvc_mixer_ui_device_get_id (output));
+        }
+        g_free (port);
+}
+
+static void
+_set_default_sink (GvcMixerControl *control,
+                   GvcMixerStream  *stream)
+{
+        guint new_id;
+
+        if (stream == NULL) {
+                /* Don't tell front-ends about an unset default
+                 * sink if it's already unset */
+                if (control->priv->default_sink_is_set == FALSE)
+                        return;
+
+                g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control),
+                                                      on_default_sink_port_notify,
+                                                      control);
+
+                control->priv->default_sink_id = 0;
+                control->priv->default_sink_is_set = FALSE;
+                g_signal_emit (control,
+                               signals[DEFAULT_SINK_CHANGED],
+                               0,
+                               PA_INVALID_INDEX);
+                return;
+        }
+
+        new_id = gvc_mixer_stream_get_id (stream);
+
+        if (control->priv->default_sink_id != new_id) {
+                GvcMixerUIDevice *output;
+                if (control->priv->default_sink_is_set) {
+                        g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control),
+                                                              on_default_sink_port_notify,
+                                                              control);
+                }
+
+                control->priv->default_sink_id = new_id;
+
+                control->priv->default_sink_is_set = TRUE;
+                g_signal_emit (control,
+                               signals[DEFAULT_SINK_CHANGED],
+                               0,
+                               new_id);
+
+                g_signal_connect (stream,
+                                  "notify::port",
+                                  G_CALLBACK (on_default_sink_port_notify),
+                                  control);
+
+                output = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+                g_debug ("active_sink change");
+
+                g_signal_emit (G_OBJECT (control),
+                               signals[ACTIVE_OUTPUT_UPDATE],
+                               0,
+                               gvc_mixer_ui_device_get_id (output));
+        }
+}
+
+static gboolean
+_stream_has_name (gpointer        key,
+                  GvcMixerStream *stream,
+                  const char     *name)
+{
+        const char *t_name;
+
+        t_name = gvc_mixer_stream_get_name (stream);
+
+        if (t_name != NULL
+            && name != NULL
+            && strcmp (t_name, name) == 0) {
+                return TRUE;
+        }
+
+        return FALSE;
+}
+
+static GvcMixerStream *
+find_stream_for_name (GvcMixerControl *control,
+                      const char      *name)
+{
+        GvcMixerStream *stream;
+
+        stream = g_hash_table_find (control->priv->all_streams,
+                                    (GHRFunc)_stream_has_name,
+                                    (char *)name);
+        return stream;
+}
+
+static void
+update_default_source_from_name (GvcMixerControl *control,
+                                 const char      *name)
+{
+        gboolean changed = FALSE;
+
+        if ((control->priv->default_source_name == NULL
+             && name != NULL)
+            || (control->priv->default_source_name != NULL
+                && name == NULL)
+            || (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) {
+                changed = TRUE;
+        }
+
+        if (changed) {
+                GvcMixerStream *stream;
+
+                g_free (control->priv->default_source_name);
+                control->priv->default_source_name = g_strdup (name);
+
+                stream = find_stream_for_name (control, name);
+                _set_default_source (control, stream);
+        }
+}
+
+static void
+update_default_sink_from_name (GvcMixerControl *control,
+                               const char      *name)
+{
+        gboolean changed = FALSE;
+
+        if ((control->priv->default_sink_name == NULL
+             && name != NULL)
+            || (control->priv->default_sink_name != NULL
+                && name == NULL)
+            || (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) {
+                changed = TRUE;
+        }
+
+        if (changed) {
+                GvcMixerStream *stream;
+                g_free (control->priv->default_sink_name);
+                control->priv->default_sink_name = g_strdup (name);
+
+                stream = find_stream_for_name (control, name);
+                _set_default_sink (control, stream);
+        }
+}
+
+static void
+update_server (GvcMixerControl      *control,
+               const pa_server_info *info)
+{
+        if (info->default_source_name != NULL) {
+                update_default_source_from_name (control, info->default_source_name);
+        }
+        if (info->default_sink_name != NULL) {
+                g_debug ("update server");
+                update_default_sink_from_name (control, info->default_sink_name);
+        }
+}
+
+static void
+remove_stream (GvcMixerControl *control,
+               GvcMixerStream  *stream)
+{
+        guint id;
+
+        g_object_ref (stream);
+
+        id = gvc_mixer_stream_get_id (stream);
+
+        if (id == control->priv->default_sink_id) {
+                _set_default_sink (control, NULL);
+        } else if (id == control->priv->default_source_id) {
+                _set_default_source (control, NULL);
+        }
+
+        g_hash_table_remove (control->priv->all_streams,
+                             GUINT_TO_POINTER (id));
+        g_signal_emit (G_OBJECT (control),
+                       signals[STREAM_REMOVED],
+                       0,
+                       gvc_mixer_stream_get_id (stream));
+        g_object_unref (stream);
+}
+
+static void
+add_stream (GvcMixerControl *control,
+            GvcMixerStream  *stream)
+{
+        g_hash_table_insert (control->priv->all_streams,
+                             GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)),
+                             stream);
+        g_signal_emit (G_OBJECT (control),
+                       signals[STREAM_ADDED],
+                       0,
+                       gvc_mixer_stream_get_id (stream));
+}
+
+/* This method will match individual stream ports against its corresponding device
+ * It does this by:
+ * - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream
+ *   and the port-name on the device is the same as the streamport-name.
+ * This should always find a match and is used exclusively by sync_devices().
+ */
+static gboolean
+match_stream_with_devices (GvcMixerControl    *control,
+                           GvcMixerStreamPort *stream_port,
+                           GvcMixerStream     *stream)
+{
+        GList                   *devices, *d;
+        guint                    stream_card_id;
+        guint                    stream_id;
+        gboolean                 in_possession = FALSE;
+
+        stream_id      =  gvc_mixer_stream_get_id (stream);
+        stream_card_id =  gvc_mixer_stream_get_card_index (stream);
+
+        devices  = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs);
+
+        for (d = devices; d != NULL; d = d->next) {
+                GvcMixerUIDevice *device;
+                guint             device_stream_id;
+                gchar            *device_port_name;
+                gchar            *origin;
+                gchar            *description;
+                GvcMixerCard     *card;
+                guint             card_id;
+
+                device = d->data;
+                g_object_get (G_OBJECT (device),
+                             "stream-id", &device_stream_id,
+                             "card", &card,
+                             "origin", &origin,
+                             "description", &description,
+                             "port-name", &device_port_name,
+                              NULL);
+
+                if (card == NULL) {
+                        if (device_stream_id == stream_id) {
+                                g_debug ("Matched stream %u with card-less device '%s', with stream already setup",
+                                         stream_id, description);
+                                in_possession = TRUE;
+                        }
+                } else {
+                        card_id = gvc_mixer_card_get_index (card);
+
+                        g_debug ("Attempt to match_stream update_with_existing_outputs - Try description : '%s', origin : '%s', device port name : '%s', card : %p, AGAINST stream port: '%s', sink card id %i",
+                                 description,
+                                 origin,
+                                 device_port_name,
+                                 card,
+                                 stream_port->port,
+                                 stream_card_id);
+
+                        if (stream_card_id == card_id &&
+                            g_strcmp0 (device_port_name, stream_port->port) == 0) {
+                                g_debug ("Match device with stream: We have a match with description: '%s', origin: '%s', cached already with device id %u, so set stream id to %i",
+                                         description,
+                                         origin,
+                                         gvc_mixer_ui_device_get_id (device),
+                                         stream_id);
+
+                                g_object_set (G_OBJECT (device),
+                                              "stream-id", stream_id,
+                                              NULL);
+                                in_possession = TRUE;
+                        }
+                }
+
+                g_free (device_port_name);
+                g_free (origin);
+                g_free (description);
+
+                if (in_possession == TRUE)
+                        break;
+        }
+
+        g_list_free (devices);
+        return in_possession;
+}
+
+/*
+ * This method attempts to match a sink or source with its relevant UI device.
+ * GvcMixerStream can represent both a sink or source.
+ * Using static card port introspection implies that we know beforehand what
+ * outputs and inputs are available to the user.
+ * But that does not mean that all of these inputs and outputs are available to be used.
+ * For instance we might be able to see that there is a HDMI port available but if
+ * we are on the default analog stereo output profile there is no valid sink for
+ * that HDMI device. We first need to change profile and when update_sink() is called
+ * only then can we match the new hdmi sink with its corresponding device.
+ *
+ * Firstly it checks to see if the incoming stream has no ports.
+ * - If a stream has no ports and no valid card id, it goes ahead and makes a new
+ *   device (software/network devices are only detectable at the sink/source level)
+ * If the stream has ports it will match each port against the stream using match_stream_with_devices().
+ *
+ * This method should always find a match.
+ */
+static void
+sync_devices (GvcMixerControl *control,
+              GvcMixerStream* stream)
+{
+        /* Go through ports to see what outputs can be created. */
+        const GList *stream_ports;
+        const GList *n = NULL;
+        gboolean     is_output = !GVC_IS_MIXER_SOURCE (stream);
+
+        stream_ports = gvc_mixer_stream_get_ports (stream);
+
+        if (stream_ports == NULL) {
+                GvcMixerUIDevice *device;
+                GObject *object;
+
+                object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
+                                       "stream-id", gvc_mixer_stream_get_id (stream),
+                                       "description", gvc_mixer_stream_get_description (stream),
+                                       "origin", "", /* Leave it empty for these special cases */
+                                       "port-name", NULL,
+                                       "port-available", TRUE,
+                                       "icon-name", gvc_mixer_stream_get_icon_name (stream),
+                                        NULL);
+                device = GVC_MIXER_UI_DEVICE (object);
+
+                g_hash_table_insert (is_output ? control->priv->ui_outputs : control->priv->ui_inputs,
+                                     GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)),
+                                     g_object_ref (device));
+
+                g_signal_emit (G_OBJECT (control),
+                               signals[is_output ? OUTPUT_ADDED : INPUT_ADDED],
+                               0,
+                               gvc_mixer_ui_device_get_id (device));
+
+                return;
+        }
+
+        /* Go ahead and make sure to match each port against a previously created device */
+        for (n = stream_ports; n != NULL; n = n->next) {
+
+                GvcMixerStreamPort *stream_port;
+                stream_port = n->data;
+
+                if (match_stream_with_devices (control, stream_port, stream))
+                        continue;
+
+                g_warning ("Sync_devices: Failed to match stream id: %u, description: '%s', origin: '%s'",
+                           gvc_mixer_stream_get_id (stream),
+                           stream_port->human_port,
+                           gvc_mixer_stream_get_description (stream));
+        }
+}
+
+static void
+set_icon_name_from_proplist (GvcMixerStream *stream,
+                             pa_proplist    *l,
+                             const char     *default_icon_name)
+{
+        const char *t;
+
+        if ((t = pa_proplist_gets (l, PA_PROP_DEVICE_ICON_NAME))) {
+                goto finish;
+        }
+
+        if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ICON_NAME))) {
+                goto finish;
+        }
+
+        if ((t = pa_proplist_gets (l, PA_PROP_WINDOW_ICON_NAME))) {
+                goto finish;
+        }
+
+        if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ICON_NAME))) {
+                goto finish;
+        }
+
+        if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) {
+
+                if (strcmp (t, "video") == 0 ||
+                    strcmp (t, "phone") == 0) {
+                        goto finish;
+                }
+
+                if (strcmp (t, "music") == 0) {
+                        t = "audio";
+                        goto finish;
+                }
+
+                if (strcmp (t, "game") == 0) {
+                        t = "applications-games";
+                        goto finish;
+                }
+
+                if (strcmp (t, "event") == 0) {
+                        t = "dialog-information";
+                        goto finish;
+                }
+        }
+
+        t = default_icon_name;
+
+ finish:
+        gvc_mixer_stream_set_icon_name (stream, t);
+}
+
+static GvcMixerStreamState
+translate_pa_state (pa_sink_state_t state) {
+        switch (state) {
+        case PA_SINK_RUNNING:
+                return GVC_STREAM_STATE_RUNNING;
+        case PA_SINK_IDLE:
+                return GVC_STREAM_STATE_IDLE;
+        case PA_SINK_SUSPENDED:
+                return GVC_STREAM_STATE_SUSPENDED;
+        case PA_SINK_INIT:
+        case PA_SINK_INVALID_STATE:
+        case PA_SINK_UNLINKED:
+        default:
+                return GVC_STREAM_STATE_INVALID;
+        }
+}
+
+/*
+ * Called when anything changes with a sink.
+ */
+static void
+update_sink (GvcMixerControl    *control,
+             const pa_sink_info *info)
+{
+        GvcMixerStream  *stream;
+        gboolean        is_new;
+        pa_volume_t     max_volume;
+        GvcChannelMap   *map;
+        char            map_buff[PA_CHANNEL_MAP_SNPRINT_MAX];
+
+        pa_channel_map_snprint (map_buff, PA_CHANNEL_MAP_SNPRINT_MAX, &info->channel_map);
+#if 1
+        g_debug ("Updating sink: index=%u name='%s' description='%s' map='%s'",
+                 info->index,
+                 info->name,
+                 info->description,
+                 map_buff);
+#endif
+
+        map = NULL;
+        is_new = FALSE;
+        stream = g_hash_table_lookup (control->priv->sinks,
+                                      GUINT_TO_POINTER (info->index));
+
+        if (stream == NULL) {
+                GList *list = NULL;
+                guint i;
+
+                map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+                stream = gvc_mixer_sink_new (control->priv->pa_context,
+                                             info->index,
+                                             map);
+
+                for (i = 0; i < info->n_ports; i++) {
+                        GvcMixerStreamPort *port;
+
+                        port = g_slice_new0 (GvcMixerStreamPort);
+                        port->port = g_strdup (info->ports[i]->name);
+                        port->human_port = g_strdup (info->ports[i]->description);
+                        port->priority = info->ports[i]->priority;
+                        port->available = info->ports[i]->available != PA_PORT_AVAILABLE_NO;
+
+                        list = g_list_prepend (list, port);
+                }
+                gvc_mixer_stream_set_ports (stream, list);
+
+                g_object_unref (map);
+                is_new = TRUE;
+
+        } else if (gvc_mixer_stream_is_running (stream)) {
+                /* Ignore events if volume changes are outstanding */
+                g_debug ("Ignoring event, volume changes are outstanding");
+                return;
+        }
+
+        max_volume = pa_cvolume_max (&info->volume);
+        gvc_mixer_stream_set_name (stream, info->name);
+        gvc_mixer_stream_set_card_index (stream, info->card);
+        gvc_mixer_stream_set_description (stream, info->description);
+        set_icon_name_from_proplist (stream, info->proplist, "audio-card");
+        gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR));
+        gvc_mixer_stream_set_sysfs_path (stream, pa_proplist_gets (info->proplist, "sysfs.path"));
+        gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+        gvc_mixer_stream_set_is_muted (stream, info->mute);
+        gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SINK_DECIBEL_VOLUME));
+        gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume);
+        gvc_mixer_stream_set_state (stream, translate_pa_state (info->state));
+
+        /* Messy I know but to set the port everytime regardless of whether it has changed will cost us a
+         * port change notify signal which causes the frontend to resync.
+         * Only update the UI when something has changed. */
+        if (info->active_port != NULL) {
+                if (is_new)
+                        gvc_mixer_stream_set_port (stream, info->active_port->name);
+                else {
+                        const GvcMixerStreamPort *active_port;
+                        active_port = gvc_mixer_stream_get_port (stream);
+                        if (active_port == NULL ||
+                            g_strcmp0 (active_port->port, info->active_port->name) != 0) {
+                                g_debug ("update sink - apparently a port update");
+                                gvc_mixer_stream_set_port (stream, info->active_port->name);
+                        }
+                }
+        }
+
+        if (is_new) {
+                g_debug ("update sink - is new");
+
+                g_hash_table_insert (control->priv->sinks,
+                                     GUINT_TO_POINTER (info->index),
+                                     g_object_ref (stream));
+                add_stream (control, stream);
+                /* Always sink on a new stream to able to assign the right stream id
+                 * to the appropriate outputs (multiple potential outputs per stream). */
+                sync_devices (control, stream);
+        } else {
+                g_signal_emit (G_OBJECT (control),
+                               signals[STREAM_CHANGED],
+                               0,
+                               gvc_mixer_stream_get_id (stream));
+        }
+
+        /*
+         * When we change profile on a device that is not the server default sink,
+         * it will jump back to the default sink set by the server to prevent the audio setup from being 'outputless'.
+         * All well and good but then when we get the new stream created for the new profile how do we know
+         * that this is the intended default or selected device the user wishes to use.
+         * This is messy but it's the only reliable way that it can be done without ripping the whole thing apart.
+         */
+        if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) {
+                GvcMixerUIDevice *dev = NULL;
+                dev = gvc_mixer_control_lookup_output_id (control, control->priv->profile_swapping_device_id);
+                if (dev != NULL) {
+                        /* now check to make sure this new stream is the same stream just matched and set on the device object */
+                        if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) {
+                                g_debug ("Looks like we profile swapped on a non server default sink");
+                                gvc_mixer_control_set_default_sink (control, stream);
+                                control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
+                        }
+                }
+        }
+
+        if (control->priv->default_sink_name != NULL
+            && info->name != NULL
+            && strcmp (control->priv->default_sink_name, info->name) == 0) {
+                _set_default_sink (control, stream);
+        }
+
+        if (map == NULL)
+                map = (GvcChannelMap *) gvc_mixer_stream_get_channel_map (stream);
+
+        gvc_channel_map_volume_changed (map, &info->volume, FALSE);
+}
+
+static void
+update_source (GvcMixerControl      *control,
+               const pa_source_info *info)
+{
+        GvcMixerStream *stream;
+        gboolean        is_new;
+        pa_volume_t     max_volume;
+
+#if 1
+        g_debug ("Updating source: index=%u name='%s' description='%s'",
+                 info->index,
+                 info->name,
+                 info->description);
+#endif
+
+        /* completely ignore monitors, they're not real sources */
+        if (info->monitor_of_sink != PA_INVALID_INDEX) {
+                return;
+        }
+
+        is_new = FALSE;
+
+        stream = g_hash_table_lookup (control->priv->sources,
+                                      GUINT_TO_POINTER (info->index));
+        if (stream == NULL) {
+                GList *list = NULL;
+                guint i;
+                GvcChannelMap *map;
+
+                map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+                stream = gvc_mixer_source_new (control->priv->pa_context,
+                                               info->index,
+                                               map);
+
+                for (i = 0; i < info->n_ports; i++) {
+                        GvcMixerStreamPort *port;
+
+                        port = g_slice_new0 (GvcMixerStreamPort);
+                        port->port = g_strdup (info->ports[i]->name);
+                        port->human_port = g_strdup (info->ports[i]->description);
+                        port->priority = info->ports[i]->priority;
+                        list = g_list_prepend (list, port);
+                }
+                gvc_mixer_stream_set_ports (stream, list);
+
+                g_object_unref (map);
+                is_new = TRUE;
+        } else if (gvc_mixer_stream_is_running (stream)) {
+                /* Ignore events if volume changes are outstanding */
+                g_debug ("Ignoring event, volume changes are outstanding");
+                return;
+        }
+
+        max_volume = pa_cvolume_max (&info->volume);
+
+        gvc_mixer_stream_set_name (stream, info->name);
+        gvc_mixer_stream_set_card_index (stream, info->card);
+        gvc_mixer_stream_set_description (stream, info->description);
+        set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone");
+        gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR));
+        gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+        gvc_mixer_stream_set_is_muted (stream, info->mute);
+        gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SOURCE_DECIBEL_VOLUME));
+        gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume);
+        g_debug ("update source");
+
+        if (info->active_port != NULL) {
+                if (is_new)
+                        gvc_mixer_stream_set_port (stream, info->active_port->name);
+                else {
+                        const GvcMixerStreamPort *active_port;
+                        active_port = gvc_mixer_stream_get_port (stream);
+                        if (active_port == NULL ||
+                            g_strcmp0 (active_port->port, info->active_port->name) != 0) {
+                                g_debug ("update source - apparently a port update");
+                                gvc_mixer_stream_set_port (stream, info->active_port->name);
+                        }
+                }
+        }
+
+        if (is_new) {
+                g_hash_table_insert (control->priv->sources,
+                                     GUINT_TO_POINTER (info->index),
+                                     g_object_ref (stream));
+                add_stream (control, stream);
+                sync_devices (control, stream);
+        } else {
+                g_signal_emit (G_OBJECT (control),
+                               signals[STREAM_CHANGED],
+                               0,
+                               gvc_mixer_stream_get_id (stream));
+        }
+
+        if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) {
+                GvcMixerUIDevice *dev = NULL;
+
+                dev = gvc_mixer_control_lookup_input_id (control, control->priv->profile_swapping_device_id);
+
+                if (dev != NULL) {
+                        /* now check to make sure this new stream is the same stream just matched and set on the device object */
+                        if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) {
+                                g_debug ("Looks like we profile swapped on a non server default source");
+                                gvc_mixer_control_set_default_source (control, stream);
+                                control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
+                        }
+                }
+        }
+        if (control->priv->default_source_name != NULL
+            && info->name != NULL
+            && strcmp (control->priv->default_source_name, info->name) == 0) {
+                _set_default_source (control, stream);
+        }
+}
+
+static void
+set_is_event_stream_from_proplist (GvcMixerStream *stream,
+                                   pa_proplist    *l)
+{
+        const char *t;
+        gboolean is_event_stream;
+
+        is_event_stream = FALSE;
+
+        if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) {
+                if (g_str_equal (t, "event"))
+                        is_event_stream = TRUE;
+        }
+
+        gvc_mixer_stream_set_is_event_stream (stream, is_event_stream);
+}
+
+static void
+set_application_id_from_proplist (GvcMixerStream *stream,
+                                  pa_proplist    *l)
+{
+        const char *t;
+
+        if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ID))) {
+                gvc_mixer_stream_set_application_id (stream, t);
+        }
+}
+
+static void
+update_sink_input (GvcMixerControl          *control,
+                   const pa_sink_input_info *info)
+{
+        GvcMixerStream *stream;
+        gboolean        is_new;
+        pa_volume_t     max_volume;
+        const char     *name;
+
+#if 0
+        g_debug ("Updating sink input: index=%u name='%s' client=%u sink=%u",
+                 info->index,
+                 info->name,
+                 info->client,
+                 info->sink);
+#endif
+
+        is_new = FALSE;
+
+        stream = g_hash_table_lookup (control->priv->sink_inputs,
+                                      GUINT_TO_POINTER (info->index));
+        if (stream == NULL) {
+                GvcChannelMap *map;
+                map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+                stream = gvc_mixer_sink_input_new (control->priv->pa_context,
+                                                   info->index,
+                                                   map);
+                g_object_unref (map);
+                is_new = TRUE;
+        } else if (gvc_mixer_stream_is_running (stream)) {
+                /* Ignore events if volume changes are outstanding */
+                g_debug ("Ignoring event, volume changes are outstanding");
+                return;
+        }
+
+        max_volume = pa_cvolume_max (&info->volume);
+
+        name = (const char *)g_hash_table_lookup (control->priv->clients,
+                                                  GUINT_TO_POINTER (info->client));
+        gvc_mixer_stream_set_name (stream, name);
+        gvc_mixer_stream_set_description (stream, info->name);
+
+        set_application_id_from_proplist (stream, info->proplist);
+        set_is_event_stream_from_proplist (stream, info->proplist);
+        set_icon_name_from_proplist (stream, info->proplist, "application-x-executable");
+        gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+        gvc_mixer_stream_set_is_muted (stream, info->mute);
+        gvc_mixer_stream_set_is_virtual (stream, info->client == PA_INVALID_INDEX);
+
+        if (is_new) {
+                g_hash_table_insert (control->priv->sink_inputs,
+                                     GUINT_TO_POINTER (info->index),
+                                     g_object_ref (stream));
+                add_stream (control, stream);
+        } else {
+                g_signal_emit (G_OBJECT (control),
+                               signals[STREAM_CHANGED],
+                               0,
+                               gvc_mixer_stream_get_id (stream));
+        }
+}
+
+static void
+update_source_output (GvcMixerControl             *control,
+                      const pa_source_output_info *info)
+{
+        GvcMixerStream *stream;
+        gboolean        is_new;
+        pa_volume_t     max_volume;
+        const char     *name;
+
+#if 1
+        g_debug ("Updating source output: index=%u name='%s' client=%u source=%u",
+                 info->index,
+                 info->name,
+                 info->client,
+                 info->source);
+#endif
+
+        is_new = FALSE;
+        stream = g_hash_table_lookup (control->priv->source_outputs,
+                                      GUINT_TO_POINTER (info->index));
+        if (stream == NULL) {
+                GvcChannelMap *map;
+                map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+                stream = gvc_mixer_source_output_new (control->priv->pa_context,
+                                                      info->index,
+                                                      map);
+                g_object_unref (map);
+                is_new = TRUE;
+        }
+
+        name = (const char *)g_hash_table_lookup (control->priv->clients,
+                                                  GUINT_TO_POINTER (info->client));
+
+        max_volume = pa_cvolume_max (&info->volume);
+
+        gvc_mixer_stream_set_name (stream, name);
+        gvc_mixer_stream_set_description (stream, info->name);
+        set_application_id_from_proplist (stream, info->proplist);
+        set_is_event_stream_from_proplist (stream, info->proplist);
+        gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+        gvc_mixer_stream_set_is_muted (stream, info->mute);
+        set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone");
+
+        if (is_new) {
+                g_hash_table_insert (control->priv->source_outputs,
+                                     GUINT_TO_POINTER (info->index),
+                                     g_object_ref (stream));
+                add_stream (control, stream);
+        } else {
+                g_signal_emit (G_OBJECT (control),
+                               signals[STREAM_CHANGED],
+                               0,
+                               gvc_mixer_stream_get_id (stream));
+        }
+}
+
+static void
+update_client (GvcMixerControl      *control,
+               const pa_client_info *info)
+{
+#if 1
+        g_debug ("Updating client: index=%u name='%s'",
+                 info->index,
+                 info->name);
+#endif
+        g_hash_table_insert (control->priv->clients,
+                             GUINT_TO_POINTER (info->index),
+                             g_strdup (info->name));
+}
+
+static char *
+card_num_streams_to_status (guint sinks,
+                            guint sources)
+{
+        char *sinks_str;
+        char *sources_str;
+        char *ret;
+
+        if (sinks == 0 && sources == 0) {
+                /* translators:
+                 * The device has been disabled */
+                return g_strdup (_("Disabled"));
+        }
+        if (sinks == 0) {
+                sinks_str = NULL;
+        } else {
+                /* translators:
+                 * The number of sound outputs on a particular device */
+                sinks_str = g_strdup_printf (ngettext ("%u Output",
+                                                       "%u Outputs",
+                                                       sinks),
+                                             sinks);
+        }
+        if (sources == 0) {
+                sources_str = NULL;
+        } else {
+                /* translators:
+                 * The number of sound inputs on a particular device */
+                sources_str = g_strdup_printf (ngettext ("%u Input",
+                                                         "%u Inputs",
+                                                         sources),
+                                               sources);
+        }
+        if (sources_str == NULL)
+                return sinks_str;
+        if (sinks_str == NULL)
+                return sources_str;
+        ret = g_strdup_printf ("%s / %s", sinks_str, sources_str);
+        g_free (sinks_str);
+        g_free (sources_str);
+        return ret;
+}
+
+/*
+ * A utility method to gather which card profiles are relevant to the port .
+ */
+static GList *
+determine_profiles_for_port (pa_card_port_info *port,
+                             const GList       *card_profiles)
+{
+        guint i;
+        GList *supported_profiles = NULL;
+        const GList *p;
+        for (i = 0; i < port->n_profiles; i++) {
+                for (p = card_profiles; p != NULL; p = p->next) {
+                        GvcMixerCardProfile *prof;
+                        prof = p->data;
+                        if (g_strcmp0 (port->profiles[i]->name, prof->profile) == 0)
+                                supported_profiles = g_list_append (supported_profiles, prof);
+                }
+        }
+        g_debug ("%i profiles supported on port %s",
+                 g_list_length (supported_profiles),
+                 port->description);
+        return g_list_sort (supported_profiles, (GCompareFunc) gvc_mixer_card_profile_compare);
+}
+
+static gboolean
+is_card_port_an_output (GvcMixerCardPort* port)
+{
+        return port->direction == PA_DIRECTION_OUTPUT ? TRUE : FALSE;
+}
+
+/*
+ * This method will create a ui device for the given port.
+ */
+static void
+update_ui_device_on_port_added (GvcMixerControl  *control,
+                                GvcMixerCardPort *port,
+                                GvcMixerCard     *card)
+{
+        GvcMixerUIDeviceDirection  direction;
+        GObject                   *object;
+        GvcMixerUIDevice          *uidevice;
+        gboolean                   available = port->available != PA_PORT_AVAILABLE_NO;
+
+        direction = (is_card_port_an_output (port) == TRUE) ? UIDeviceOutput : UIDeviceInput;
+
+        object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
+                               "type", (guint)direction,
+                               "card", card,
+                               "port-name", port->port,
+                               "description", port->human_port,
+                               "origin", gvc_mixer_card_get_name (card),
+                               "port-available", available,
+                               "icon-name", port->icon_name,
+                               NULL);
+
+        uidevice = GVC_MIXER_UI_DEVICE (object);
+        gvc_mixer_ui_device_set_profiles (uidevice, port->profiles);
+
+        g_hash_table_insert (is_card_port_an_output (port) ? control->priv->ui_outputs : control->priv->ui_inputs,
+                             GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (uidevice)),
+                             uidevice);
+
+
+        if (available) {
+                g_signal_emit (G_OBJECT (control),
+                               signals[is_card_port_an_output (port) ? OUTPUT_ADDED : INPUT_ADDED],
+                               0,
+                               gvc_mixer_ui_device_get_id (uidevice));
+        }
+
+        g_debug ("update_ui_device_on_port_added, direction %u, description '%s', origin '%s', port available %i", 
+                 direction,
+                 port->human_port,
+                 gvc_mixer_card_get_name (card),
+                 available);
+}
+
+static void
+update_ui_device_on_port_changed (GvcMixerControl   *control,
+                                  GvcMixerCardPort  *card_port,
+                                  pa_card_port_info *new_port_info,
+                                  GvcMixerCard      *card)
+{
+        GList                   *d;
+        GList                   *devices;
+        GvcMixerUIDevice        *device;
+        gboolean                 is_output = is_card_port_an_output (card_port);
+
+        devices  = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs);
+
+        for (d = devices; d != NULL; d = d->next) {
+                GvcMixerCard *device_card;
+                gchar        *device_port_name;
+
+                device = d->data;
+                g_object_get (G_OBJECT (device),
+                             "card", &device_card,
+                             "port-name", &device_port_name,
+                              NULL);
+
+                if (g_strcmp0 (card_port->port, device_port_name) == 0 &&
+                        device_card == card) {
+                        gboolean was_available;
+                        gboolean is_available;
+                        const GList *card_profiles = gvc_mixer_card_get_profiles (card);
+
+                        was_available = card_port->available != PA_PORT_AVAILABLE_NO;
+                        is_available = new_port_info->available != PA_PORT_AVAILABLE_NO;
+
+                        g_debug ("Found the relevant device %s, update its port availability flag to %i, is_output %i",
+                                 device_port_name,
+                                 is_available,
+                                 is_output);
+
+                        card_port->available = new_port_info->available;
+
+                        g_list_free (card_port->profiles);
+                        card_port->profiles = determine_profiles_for_port (new_port_info, card_profiles);
+
+                        gvc_mixer_ui_device_set_profiles (device, card_port->profiles);
+
+                        if (is_available != was_available) {
+                                g_object_set (G_OBJECT (device),
+                                              "port-available", is_available, NULL);
+                                g_signal_emit (G_OBJECT (control),
+                                               is_output ? signals[is_available ? OUTPUT_ADDED : OUTPUT_REMOVED]
+                                                         : signals[is_available ? INPUT_ADDED : INPUT_REMOVED],
+                                               0,
+                                               gvc_mixer_ui_device_get_id (device));
+                        }
+               }
+               g_free (device_port_name);
+        }
+
+        g_list_free (devices);
+}
+
+static void
+maybe_remove_ui_device (GvcMixerControl  *control,
+                        GvcMixerUIDevice *device)
+{
+        /* We add UIDevices for ports or for streams, so remove them if the device now
+         * has neither.
+         */
+        if (gvc_mixer_ui_device_get_stream_id (device) == GVC_MIXER_UI_DEVICE_INVALID &&
+            !gvc_mixer_ui_device_has_ports (device)) {
+                gboolean is_output = gvc_mixer_ui_device_is_output (device);
+
+                g_debug ("Removing UIDevice %s",
+                         gvc_mixer_ui_device_get_description (device));
+
+                g_hash_table_remove (is_output ? control->priv->ui_outputs : control->priv->ui_inputs,
+                                     GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)));
+        }
+}
+
+static void
+update_ui_device_on_port_removed (GvcMixerControl  *control,
+                                  GvcMixerCardPort *card_port,
+                                  GvcMixerCard     *card)
+{
+        GList *d;
+        GList *devices;
+        gboolean is_output = is_card_port_an_output (card_port);
+
+        devices  = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs);
+        for (d = devices; d != NULL; d = d->next) {
+                GvcMixerUIDevice *device = d->data;
+                GvcMixerCard *device_card;
+                gchar *device_port_name;
+
+                g_object_get (G_OBJECT (device),
+                             "card", &device_card,
+                             "port-name", &device_port_name,
+                              NULL);
+
+                if (g_strcmp0 (card_port->port, device_port_name) == 0 && device_card == card) {
+                        g_object_set (G_OBJECT (device),
+                                      "card", NULL,
+                                      "port-name", NULL,
+                                       NULL);
+
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[is_output ? OUTPUT_REMOVED : INPUT_REMOVED],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (device));
+
+                        maybe_remove_ui_device (control, device);
+                }
+
+               g_free (device_port_name);
+        }
+
+        g_list_free (devices);
+}
+
+#ifdef HAVE_ALSA
+typedef struct {
+        char *port_name_to_set;
+        guint32 headset_card;
+} PortStatusData;
+
+static void
+port_status_data_free (PortStatusData *data)
+{
+        if (data == NULL)
+                return;
+        g_free (data->port_name_to_set);
+        g_free (data);
+}
+
+/*
+ We need to re-enumerate sources and sinks every time the user makes a choice,
+ because they can change due to use interaction in other software (or policy
+ changes inside PulseAudio). Enumeration means PulseAudio will do a series of
+ callbacks, one for every source/sink.
+ Set the port when we find the correct source/sink.
+ */
+
+static void
+sink_info_cb (pa_context         *c,
+              const pa_sink_info *i,
+              int                 eol,
+              void               *userdata)
+{
+        PortStatusData *data = userdata;
+        pa_operation *o;
+        guint j;
+        const char *s;
+
+        if (eol != 0) {
+                port_status_data_free (data);
+                return;
+        }
+
+        if (i->card != data->headset_card)
+                return;
+
+        s = data->port_name_to_set;
+
+        if (i->active_port &&
+            strcmp (i->active_port->name, s) == 0)
+                return;
+
+        for (j = 0; j < i->n_ports; j++)
+                if (strcmp (i->ports[j]->name, s) == 0)
+                        break;
+
+        if (j >= i->n_ports)
+                return;
+
+        o = pa_context_set_sink_port_by_index (c, i->index, s, NULL, NULL);
+        g_clear_pointer (&o, pa_operation_unref);
+}
+
+static void
+source_info_cb (pa_context           *c,
+                const pa_source_info *i,
+                int                   eol,
+                void                 *userdata)
+{
+        PortStatusData *data = userdata;
+        pa_operation *o;
+        guint j;
+        const char *s;
+
+        if (eol != 0) {
+                port_status_data_free (data);
+                return;
+        }
+
+        if (i->card != data->headset_card)
+                return;
+
+        s = data->port_name_to_set;
+
+        for (j = 0; j < i->n_ports; j++) {
+                if (g_str_equal (i->ports[j]->name, s)) {
+                        o = pa_context_set_default_source (c,
+                                                           i->name,
+                                                           NULL,
+                                                           NULL);
+                        if (o == NULL) {
+                                g_warning ("pa_context_set_default_source() failed");
+                                return;
+                        }
+                }
+	}
+
+        if (i->active_port && strcmp (i->active_port->name, s) == 0)
+                return;
+
+        for (j = 0; j < i->n_ports; j++)
+                if (strcmp (i->ports[j]->name, s) == 0)
+                        break;
+
+        if (j >= i->n_ports)
+                return;
+
+        o = pa_context_set_source_port_by_index(c, i->index, s, NULL, NULL);
+        g_clear_pointer (&o, pa_operation_unref);
+}
+
+static void
+gvc_mixer_control_set_port_status_for_headset (GvcMixerControl *control,
+                                               guint            id,
+                                               const char      *port_name,
+                                               gboolean         is_output)
+{
+        pa_operation *o;
+        PortStatusData *data;
+
+        if (port_name == NULL)
+                return;
+
+        data = g_new0 (PortStatusData, 1);
+        data->port_name_to_set = g_strdup (port_name);
+        data->headset_card = id;
+
+        if (is_output)
+                o = pa_context_get_sink_info_list (control->priv->pa_context, sink_info_cb, data);
+        else
+                o = pa_context_get_source_info_list (control->priv->pa_context, source_info_cb, data);
+
+        g_clear_pointer (&o, pa_operation_unref);
+}
+#endif /* HAVE_ALSA */
+
+static void
+free_priv_port_names (GvcMixerControl    *control)
+{
+#ifdef HAVE_ALSA
+        g_clear_pointer (&control->priv->headphones_name, g_free);
+        g_clear_pointer (&control->priv->headsetmic_name, g_free);
+        g_clear_pointer (&control->priv->headphonemic_name, g_free);
+        g_clear_pointer (&control->priv->internalspk_name, g_free);
+        g_clear_pointer (&control->priv->internalmic_name, g_free);
+#endif
+}
+
+void
+gvc_mixer_control_set_headset_port (GvcMixerControl      *control,
+                                    guint                 id,
+                                    GvcHeadsetPortChoice  choice)
+{
+        g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
+
+#ifdef HAVE_ALSA
+        switch (choice) {
+        case GVC_HEADSET_PORT_CHOICE_HEADPHONES:
+                gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE);
+                gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalmic_name, FALSE);
+                break;
+        case GVC_HEADSET_PORT_CHOICE_HEADSET:
+                gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE);
+                gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headsetmic_name, FALSE);
+                break;
+        case GVC_HEADSET_PORT_CHOICE_MIC:
+                gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalspk_name, TRUE);
+                gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphonemic_name, FALSE);
+                break;
+        case GVC_HEADSET_PORT_CHOICE_NONE:
+        default:
+                g_assert_not_reached ();
+        }
+#else
+        g_warning ("BUG: libgnome-volume-control compiled without ALSA support");
+#endif /* HAVE_ALSA */
+}
+
+#ifdef HAVE_ALSA
+typedef struct {
+        const pa_card_port_info *headphones;
+        const pa_card_port_info *headsetmic;
+        const pa_card_port_info *headphonemic;
+        const pa_card_port_info *internalmic;
+        const pa_card_port_info *internalspk;
+} headset_ports;
+
+/*
+   In PulseAudio without ucm, ports will show up with the following names:
+   Headphones - analog-output-headphones
+   Headset mic - analog-input-headset-mic (was: analog-input-microphone-headset)
+   Jack in mic-in mode - analog-input-headphone-mic (was: analog-input-microphone)
+
+   However, since regular mics also show up as analog-input-microphone,
+   we need to check for certain controls on alsa mixer level too, to know
+   if we deal with a separate mic jack, or a multi-function jack with a
+   mic-in mode (also called "headphone mic").
+   We check for the following names:
+
+   Headphone Mic Jack - indicates headphone and mic-in mode share the same jack,
+     i e, not two separate jacks. Hardware cannot distinguish between a
+     headphone and a mic.
+   Headset Mic Phantom Jack - indicates headset jack where hardware can not
+     distinguish between headphones and headsets
+   Headset Mic Jack - indicates headset jack where hardware can distinguish
+     between headphones and headsets. There is no use popping up a dialog in
+     this case, unless we already need to do this for the mic-in mode.
+
+   From the PA_PROCOTOL_VERSION=34, The device_port structure adds 2 members
+   availability_group and type, with the help of these 2 members, we could
+   consolidate the port checking and port setting for non-ucm and with-ucm
+   cases.
+*/
+
+#define HEADSET_PORT_SET(dst, src) \
+        do { \
+                if (!(dst) || (dst)->priority < (src)->priority) \
+                        dst = src; \
+        } while (0)
+
+#define GET_PORT_NAME(x) (x ? g_strdup (x->name) : NULL)
+
+static headset_ports *
+get_headset_ports (GvcMixerControl    *control,
+                   const pa_card_info *c)
+{
+        headset_ports *h;
+        guint i;
+
+        h = g_new0 (headset_ports, 1);
+
+        for (i = 0; i < c->n_ports; i++) {
+                pa_card_port_info *p = c->ports[i];
+                if (control->priv->server_protocol_version < 34) {
+                        if (g_str_equal (p->name, "analog-output-headphones"))
+                                h->headphones = p;
+                        else if (g_str_equal (p->name, "analog-input-headset-mic"))
+                                h->headsetmic = p;
+                        else if (g_str_equal (p->name, "analog-input-headphone-mic"))
+                                h->headphonemic = p;
+                        else if (g_str_equal (p->name, "analog-input-internal-mic"))
+                                h->internalmic = p;
+                        else if (g_str_equal (p->name, "analog-output-speaker"))
+                                h->internalspk = p;
+                } else {
+#if (PA_PROTOCOL_VERSION >= 34)
+                        /* in the first loop, set only headphones */
+                        /* the microphone ports are assigned in the second loop */
+                        if (p->type == PA_DEVICE_PORT_TYPE_HEADPHONES) {
+                                if (p->availability_group)
+                                        HEADSET_PORT_SET (h->headphones, p);
+                        } else if (p->type == PA_DEVICE_PORT_TYPE_SPEAKER) {
+                                HEADSET_PORT_SET (h->internalspk, p);
+                        } else if (p->type == PA_DEVICE_PORT_TYPE_MIC) {
+                                if (!p->availability_group)
+                                        HEADSET_PORT_SET (h->internalmic, p);
+                        }
+#else
+                        g_warning_once ("libgnome-volume-control running against PulseAudio %u, "
+                                        "but compiled against older %d, report a bug to your distribution",
+                                        control->priv->server_protocol_version,
+                                        PA_PROTOCOL_VERSION);
+#endif
+                }
+        }
+
+#if (PA_PROTOCOL_VERSION >= 34)
+        if (h->headphones && (control->priv->server_protocol_version >= 34)) {
+                for (i = 0; i < c->n_ports; i++) {
+                        pa_card_port_info *p = c->ports[i];
+                        if (g_strcmp0(h->headphones->availability_group, p->availability_group))
+                                continue;
+                        if (p->direction != PA_DIRECTION_INPUT)
+                                continue;
+                        if (p->type == PA_DEVICE_PORT_TYPE_HEADSET)
+                                HEADSET_PORT_SET (h->headsetmic, p);
+                        else if (p->type == PA_DEVICE_PORT_TYPE_MIC)
+                                HEADSET_PORT_SET (h->headphonemic, p);
+                }
+        }
+#endif
+
+        return h;
+}
+
+static gboolean
+verify_alsa_card (int       cardindex,
+                  gboolean *headsetmic,
+                  gboolean *headphonemic)
+{
+        char *ctlstr;
+        snd_hctl_t *hctl;
+        snd_ctl_elem_id_t *id;
+        int err;
+
+        *headsetmic = FALSE;
+        *headphonemic = FALSE;
+
+        ctlstr = g_strdup_printf ("hw:%i", cardindex);
+        if ((err = snd_hctl_open (&hctl, ctlstr, 0)) < 0) {
+                g_warning ("snd_hctl_open failed: %s", snd_strerror(err));
+                g_free (ctlstr);
+                return FALSE;
+        }
+        g_free (ctlstr);
+
+        if ((err = snd_hctl_load (hctl)) < 0) {
+                g_warning ("snd_hctl_load failed: %s", snd_strerror(err));
+                snd_hctl_close (hctl);
+                return FALSE;
+        }
+
+        snd_ctl_elem_id_alloca (&id);
+
+        snd_ctl_elem_id_clear (id);
+        snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+        snd_ctl_elem_id_set_name (id, "Headphone Mic Jack");
+        if (snd_hctl_find_elem (hctl, id))
+                *headphonemic = TRUE;
+
+        snd_ctl_elem_id_clear (id);
+        snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+        snd_ctl_elem_id_set_name (id, "Headset Mic Phantom Jack");
+        if (snd_hctl_find_elem (hctl, id))
+                *headsetmic = TRUE;
+
+        if (*headphonemic) {
+                snd_ctl_elem_id_clear (id);
+                snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+                snd_ctl_elem_id_set_name (id, "Headset Mic Jack");
+                if (snd_hctl_find_elem (hctl, id))
+                        *headsetmic = TRUE;
+        }
+
+        snd_hctl_close (hctl);
+        return *headsetmic || *headphonemic;
+}
+
+static void
+check_audio_device_selection_needed (GvcMixerControl    *control,
+                                     const pa_card_info *info)
+{
+        headset_ports *h;
+        gboolean start_dialog, stop_dialog;
+
+        start_dialog = FALSE;
+        stop_dialog = FALSE;
+        h = get_headset_ports (control, info);
+
+        if (!h->headphones ||
+            (!h->headsetmic && !h->headphonemic)) {
+                /* Not a headset jack */
+                goto out;
+        }
+
+        if (control->priv->headset_card != (int) info->index) {
+                int cardindex;
+                gboolean hsmic = TRUE;
+                gboolean hpmic = TRUE;
+                const char *s;
+
+                s = pa_proplist_gets (info->proplist, "alsa.card");
+                if (!s)
+                        goto out;
+
+                cardindex = strtol (s, NULL, 10);
+                if (cardindex == 0 && strcmp(s, "0") != 0)
+                        goto out;
+
+                if (control->priv->server_protocol_version < 34) {
+                        if (!verify_alsa_card(cardindex, &hsmic, &hpmic))
+                                goto out;
+                }
+
+                control->priv->headset_card = info->index;
+                control->priv->has_headsetmic = hsmic && h->headsetmic;
+                control->priv->has_headphonemic = hpmic && h->headphonemic;
+        } else {
+                start_dialog = (h->headphones->available != PA_PORT_AVAILABLE_NO) && !control->priv->headset_plugged_in;
+                stop_dialog = (h->headphones->available == PA_PORT_AVAILABLE_NO) && control->priv->headset_plugged_in;
+        }
+
+        control->priv->headset_plugged_in = h->headphones->available != PA_PORT_AVAILABLE_NO;
+        free_priv_port_names (control);
+        control->priv->headphones_name = GET_PORT_NAME(h->headphones);
+        control->priv->headsetmic_name = GET_PORT_NAME(h->headsetmic);
+        control->priv->headphonemic_name = GET_PORT_NAME(h->headphonemic);
+        control->priv->internalspk_name = GET_PORT_NAME(h->internalspk);
+        control->priv->internalmic_name = GET_PORT_NAME(h->internalmic);
+
+        if (!start_dialog &&
+            !stop_dialog)
+                goto out;
+
+        if (stop_dialog) {
+                g_signal_emit (G_OBJECT (control),
+                               signals[AUDIO_DEVICE_SELECTION_NEEDED],
+                               0,
+                               info->index,
+                               FALSE,
+                               GVC_HEADSET_PORT_CHOICE_NONE);
+        } else {
+                GvcHeadsetPortChoice choices;
+
+                choices = GVC_HEADSET_PORT_CHOICE_HEADPHONES;
+                if (control->priv->has_headsetmic)
+                        choices |= GVC_HEADSET_PORT_CHOICE_HEADSET;
+                if (control->priv->has_headphonemic)
+                        choices |= GVC_HEADSET_PORT_CHOICE_MIC;
+
+                g_signal_emit (G_OBJECT (control),
+                               signals[AUDIO_DEVICE_SELECTION_NEEDED],
+                               0,
+                               info->index,
+                               TRUE,
+                               choices);
+        }
+
+out:
+        g_free (h);
+}
+#endif /* HAVE_ALSA */
+
+/*
+ * At this point we can determine all devices available to us (besides network 'ports')
+ * This is done by the following:
+ *
+ * - gvc_mixer_card and gvc_mixer_card_ports are created and relevant setters are called.
+ * - If it's a 'normal' card with ports it will create a new ui-device or
+ *   synchronise port availability with the existing device cached for that port on this card. */
+
+static void
+update_card (GvcMixerControl      *control,
+             const pa_card_info   *info)
+{
+        const GList  *m = NULL;
+        GvcMixerCard *card;
+        gboolean      is_new = FALSE;
+        GList *profile_list = NULL;
+        GList *old_ports;
+#if 1
+        guint i;
+        const char *key;
+        void *state;
+
+        g_debug ("Updating card %s (index: %u driver: %s):",
+                 info->name, info->index, info->driver);
+
+        for (i = 0; i < info->n_profiles; i++) {
+                struct pa_card_profile_info pi = info->profiles[i];
+                gboolean is_default;
+
+                is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0);
+                g_debug ("\tProfile '%s': %d sources %d sinks%s",
+                         pi.name, pi.n_sources, pi.n_sinks,
+                         is_default ? " (Current)" : "");
+        }
+        state = NULL;
+        key = pa_proplist_iterate (info->proplist, &state);
+        while (key != NULL) {
+                g_debug ("\tProperty: '%s' = '%s'",
+                        key, pa_proplist_gets (info->proplist, key));
+                key = pa_proplist_iterate (info->proplist, &state);
+        }
+#endif
+        card = g_hash_table_lookup (control->priv->cards,
+                                    GUINT_TO_POINTER (info->index));
+        if (card == NULL) {
+                card = gvc_mixer_card_new (control->priv->pa_context,
+                                           info->index);
+                is_new = TRUE;
+        }
+
+        for (i = 0; i < info->n_profiles; i++) {
+                GvcMixerCardProfile *profile;
+                struct pa_card_profile_info pi = info->profiles[i];
+
+                profile = g_new0 (GvcMixerCardProfile, 1);
+                profile->profile = g_strdup (pi.name);
+                profile->human_profile = g_strdup (pi.description);
+                profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources);
+                profile->n_sinks = pi.n_sinks;
+                profile->n_sources = pi.n_sources;
+                profile->priority = pi.priority;
+                profile_list = g_list_prepend (profile_list, profile);
+        }
+
+        gvc_mixer_card_set_profiles (card, profile_list);
+        gvc_mixer_card_set_name (card, pa_proplist_gets (info->proplist, "device.description"));
+        gvc_mixer_card_set_icon_name (card, pa_proplist_gets (info->proplist, "device.icon_name"));
+        gvc_mixer_card_set_profile (card, info->active_profile->name);
+
+        if (is_new) {
+                g_hash_table_insert (control->priv->cards,
+                                     GUINT_TO_POINTER (info->index),
+                                     card);
+        }
+
+        old_ports = g_list_copy ((GList *)gvc_mixer_card_get_ports (card));
+        for (m = old_ports; m; m = m->next) {
+                GvcMixerCardPort *card_port = m->data;
+                gboolean found = FALSE;
+
+                for (i = 0; i < info->n_ports; i++) {
+                        pa_card_port_info *port = info->ports[i];
+
+                        if (g_strcmp0 (card_port->port, port->name) == 0)
+                                found = TRUE;
+                }
+
+                if (!found) {
+                        update_ui_device_on_port_removed (control, card_port, card);
+                        gvc_mixer_card_remove_port (card, card_port);
+                }
+        }
+        g_clear_pointer (&old_ports, g_list_free);
+
+        for (i = 0; i < info->n_ports; i++) {
+                pa_card_port_info *port = info->ports[i];
+                gboolean found = FALSE;
+
+                for (m = gvc_mixer_card_get_ports (card); m; m = m->next) {
+                        GvcMixerCardPort *card_port = m->data;
+
+                        if (g_strcmp0 (card_port->port, port->name) == 0) {
+                                found = TRUE;
+                                update_ui_device_on_port_changed (control, card_port, port, card);
+                        }
+                }
+
+                if (!found) {
+                        GvcMixerCardPort *card_port;
+
+                        card_port = g_new0 (GvcMixerCardPort, 1);
+                        card_port->port = g_strdup (port->name);
+                        card_port->human_port = g_strdup (port->description);
+                        card_port->priority = port->priority;
+                        card_port->available = port->available;
+                        card_port->direction = port->direction;
+                        card_port->icon_name = g_strdup (pa_proplist_gets (port->proplist, "device.icon_name"));
+                        card_port->profiles = determine_profiles_for_port (port, profile_list);
+
+                        gvc_mixer_card_add_port (card, card_port);
+                        update_ui_device_on_port_added (control, card_port, card);
+                }
+        }
+
+#ifdef HAVE_ALSA
+        check_audio_device_selection_needed (control, info);
+#endif /* HAVE_ALSA */
+
+        g_signal_emit (G_OBJECT (control),
+                       signals[CARD_ADDED],
+                       0,
+                       info->index);
+}
+
+static void
+_pa_context_get_sink_info_cb (pa_context         *context,
+                              const pa_sink_info *i,
+                              int                 eol,
+                              void               *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+                        return;
+                }
+
+                g_warning ("Sink callback failure");
+                return;
+        }
+
+        if (eol > 0) {
+                dec_outstanding (control);
+                return;
+        }
+
+        update_sink (control, i);
+}
+
+static void
+_pa_context_get_source_info_cb (pa_context           *context,
+                                const pa_source_info *i,
+                                int                   eol,
+                                void                 *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+                        return;
+                }
+
+                g_warning ("Source callback failure");
+                return;
+        }
+
+        if (eol > 0) {
+                dec_outstanding (control);
+                return;
+        }
+
+        update_source (control, i);
+}
+
+static void
+_pa_context_get_sink_input_info_cb (pa_context               *context,
+                                    const pa_sink_input_info *i,
+                                    int                       eol,
+                                    void                     *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+                        return;
+                }
+
+                g_warning ("Sink input callback failure");
+                return;
+        }
+
+        if (eol > 0) {
+                dec_outstanding (control);
+                return;
+        }
+
+        update_sink_input (control, i);
+}
+
+static void
+_pa_context_get_source_output_info_cb (pa_context                  *context,
+                                       const pa_source_output_info *i,
+                                       int                          eol,
+                                       void                        *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+                        return;
+                }
+
+                g_warning ("Source output callback failure");
+                return;
+        }
+
+        if (eol > 0)  {
+                dec_outstanding (control);
+                return;
+        }
+
+        update_source_output (control, i);
+}
+
+static void
+_pa_context_get_client_info_cb (pa_context           *context,
+                                const pa_client_info *i,
+                                int                   eol,
+                                void                 *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+                        return;
+                }
+
+                g_warning ("Client callback failure");
+                return;
+        }
+
+        if (eol > 0) {
+                dec_outstanding (control);
+                return;
+        }
+
+        update_client (control, i);
+}
+
+static void
+_pa_context_get_card_info_by_index_cb (pa_context *context,
+                                       const pa_card_info *i,
+                                       int eol,
+                                       void *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                if (pa_context_errno (context) == PA_ERR_NOENTITY)
+                        return;
+
+                g_warning ("Card callback failure");
+                return;
+        }
+
+        if (eol > 0) {
+                dec_outstanding (control);
+                return;
+        }
+
+        update_card (control, i);
+}
+
+static void
+_pa_context_get_server_info_cb (pa_context           *context,
+                                const pa_server_info *i,
+                                void                 *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (i == NULL) {
+                g_warning ("Server info callback failure");
+                return;
+        }
+        g_debug ("get server info");
+        update_server (control, i);
+        dec_outstanding (control);
+}
+
+static void
+remove_event_role_stream (GvcMixerControl *control)
+{
+        g_debug ("Removing event role");
+}
+
+static void
+update_event_role_stream (GvcMixerControl                  *control,
+                          const pa_ext_stream_restore_info *info)
+{
+        GvcMixerStream *stream;
+        gboolean        is_new;
+        pa_volume_t     max_volume;
+
+        if (strcmp (info->name, "sink-input-by-media-role:event") != 0) {
+                return;
+        }
+
+#if 0
+        g_debug ("Updating event role: name='%s' device='%s'",
+                 info->name,
+                 info->device);
+#endif
+
+        is_new = FALSE;
+
+        if (!control->priv->event_sink_input_is_set) {
+                pa_channel_map pa_map;
+                GvcChannelMap *map;
+
+                pa_map.channels = 1;
+                pa_map.map[0] = PA_CHANNEL_POSITION_MONO;
+                map = gvc_channel_map_new_from_pa_channel_map (&pa_map);
+
+                stream = gvc_mixer_event_role_new (control->priv->pa_context,
+                                                   info->device,
+                                                   map);
+                control->priv->event_sink_input_id = gvc_mixer_stream_get_id (stream);
+                control->priv->event_sink_input_is_set = TRUE;
+
+                is_new = TRUE;
+        } else {
+                stream = g_hash_table_lookup (control->priv->all_streams,
+                                              GUINT_TO_POINTER (control->priv->event_sink_input_id));
+        }
+
+        /* 0 channels here means there is no valid volume in
+         * pa_ext_stream_restore_info() and in the saved stream entry of
+         * module-stream-restore in PulseAudio. */
+        if (info->volume.channels == 0)
+                max_volume = PA_VOLUME_NORM;
+        else
+                max_volume = pa_cvolume_max (&info->volume);
+
+        gvc_mixer_stream_set_name (stream, _("System Sounds"));
+        gvc_mixer_stream_set_icon_name (stream, "audio-x-generic");
+        gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+        gvc_mixer_stream_set_is_muted (stream, info->mute);
+
+        if (is_new) {
+                add_stream (control, stream);
+        }
+}
+
+static void
+_pa_ext_stream_restore_read_cb (pa_context                       *context,
+                                const pa_ext_stream_restore_info *i,
+                                int                               eol,
+                                void                             *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        if (eol < 0) {
+                g_debug ("Failed to initialized stream_restore extension: %s",
+                         pa_strerror (pa_context_errno (context)));
+                remove_event_role_stream (control);
+                return;
+        }
+
+        if (eol > 0) {
+                dec_outstanding (control);
+                /* If we don't have an event stream to restore, then
+                 * set one up with a default 100% volume */
+                if (!control->priv->event_sink_input_is_set) {
+                        pa_ext_stream_restore_info info;
+
+                        memset (&info, 0, sizeof(info));
+                        info.name = "sink-input-by-media-role:event";
+                        info.volume.channels = 1;
+                        info.volume.values[0] = PA_VOLUME_NORM;
+                        update_event_role_stream (control, &info);
+                }
+                return;
+        }
+
+        update_event_role_stream (control, i);
+}
+
+static void
+_pa_ext_stream_restore_subscribe_cb (pa_context *context,
+                                     void       *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+        pa_operation    *o;
+
+        o = pa_ext_stream_restore_read (context,
+                                        _pa_ext_stream_restore_read_cb,
+                                        control);
+        if (o == NULL) {
+                g_warning ("pa_ext_stream_restore_read() failed");
+                return;
+        }
+
+        pa_operation_unref (o);
+}
+
+static void
+req_update_server_info (GvcMixerControl *control,
+                        int              index)
+{
+        pa_operation *o;
+
+        o = pa_context_get_server_info (control->priv->pa_context,
+                                        _pa_context_get_server_info_cb,
+                                        control);
+        if (o == NULL) {
+                g_warning ("pa_context_get_server_info() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+req_update_client_info (GvcMixerControl *control,
+                        int              index)
+{
+        pa_operation *o;
+
+        if (index < 0) {
+                o = pa_context_get_client_info_list (control->priv->pa_context,
+                                                     _pa_context_get_client_info_cb,
+                                                     control);
+        } else {
+                o = pa_context_get_client_info (control->priv->pa_context,
+                                                index,
+                                                _pa_context_get_client_info_cb,
+                                                control);
+        }
+
+        if (o == NULL) {
+                g_warning ("pa_context_client_info_list() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+req_update_card (GvcMixerControl *control,
+                 int              index)
+{
+        pa_operation *o;
+
+        if (index < 0) {
+                o = pa_context_get_card_info_list (control->priv->pa_context,
+                                                   _pa_context_get_card_info_by_index_cb,
+                                                   control);
+        } else {
+                o = pa_context_get_card_info_by_index (control->priv->pa_context,
+                                                       index,
+                                                       _pa_context_get_card_info_by_index_cb,
+                                                       control);
+        }
+
+        if (o == NULL) {
+                g_warning ("pa_context_get_card_info_by_index() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+req_update_sink_info (GvcMixerControl *control,
+                      int              index)
+{
+        pa_operation *o;
+
+        if (index < 0) {
+                o = pa_context_get_sink_info_list (control->priv->pa_context,
+                                                   _pa_context_get_sink_info_cb,
+                                                   control);
+        } else {
+                o = pa_context_get_sink_info_by_index (control->priv->pa_context,
+                                                       index,
+                                                       _pa_context_get_sink_info_cb,
+                                                       control);
+        }
+
+        if (o == NULL) {
+                g_warning ("pa_context_get_sink_info_list() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+req_update_source_info (GvcMixerControl *control,
+                        int              index)
+{
+        pa_operation *o;
+
+        if (index < 0) {
+                o = pa_context_get_source_info_list (control->priv->pa_context,
+                                                     _pa_context_get_source_info_cb,
+                                                     control);
+        } else {
+                o = pa_context_get_source_info_by_index(control->priv->pa_context,
+                                                        index,
+                                                        _pa_context_get_source_info_cb,
+                                                        control);
+        }
+
+        if (o == NULL) {
+                g_warning ("pa_context_get_source_info_list() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+req_update_sink_input_info (GvcMixerControl *control,
+                            int              index)
+{
+        pa_operation *o;
+
+        if (index < 0) {
+                o = pa_context_get_sink_input_info_list (control->priv->pa_context,
+                                                         _pa_context_get_sink_input_info_cb,
+                                                         control);
+        } else {
+                o = pa_context_get_sink_input_info (control->priv->pa_context,
+                                                    index,
+                                                    _pa_context_get_sink_input_info_cb,
+                                                    control);
+        }
+
+        if (o == NULL) {
+                g_warning ("pa_context_get_sink_input_info_list() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+req_update_source_output_info (GvcMixerControl *control,
+                               int              index)
+{
+        pa_operation *o;
+
+        if (index < 0) {
+                o = pa_context_get_source_output_info_list (control->priv->pa_context,
+                                                            _pa_context_get_source_output_info_cb,
+                                                            control);
+        } else {
+                o = pa_context_get_source_output_info (control->priv->pa_context,
+                                                       index,
+                                                       _pa_context_get_source_output_info_cb,
+                                                       control);
+        }
+
+        if (o == NULL) {
+                g_warning ("pa_context_get_source_output_info_list() failed");
+                return;
+        }
+        pa_operation_unref (o);
+}
+
+static void
+remove_client (GvcMixerControl *control,
+               guint            index)
+{
+        g_hash_table_remove (control->priv->clients,
+                             GUINT_TO_POINTER (index));
+}
+
+static void
+remove_card (GvcMixerControl *control,
+             guint            index)
+{
+
+        GList *devices, *d;
+
+        devices = g_list_concat (g_hash_table_get_values (control->priv->ui_inputs),
+                                 g_hash_table_get_values (control->priv->ui_outputs));
+
+        for (d = devices; d != NULL; d = d->next) {
+                GvcMixerCard *card;
+                GvcMixerUIDevice *device = d->data;
+
+                g_object_get (G_OBJECT (device), "card", &card, NULL);
+
+                if (card == NULL)
+                        continue;
+
+                if (gvc_mixer_card_get_index (card) == index) {
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[gvc_mixer_ui_device_is_output (device) ? OUTPUT_REMOVED : INPUT_REMOVED],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (device));
+                        g_debug ("Card removal remove device %s",
+                                 gvc_mixer_ui_device_get_description (device));
+                        g_hash_table_remove (gvc_mixer_ui_device_is_output (device) ? control->priv->ui_outputs : control->priv->ui_inputs,
+                                             GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)));
+                }
+        }
+
+        g_list_free (devices);
+
+        g_hash_table_remove (control->priv->cards,
+                             GUINT_TO_POINTER (index));
+
+        g_signal_emit (G_OBJECT (control),
+                       signals[CARD_REMOVED],
+                       0,
+                       index);
+}
+
+static void
+remove_sink (GvcMixerControl *control,
+             guint            index)
+{
+        GvcMixerStream   *stream;
+        GvcMixerUIDevice *device;
+
+        g_debug ("Removing sink: index=%u", index);
+
+        stream = g_hash_table_lookup (control->priv->sinks,
+                                      GUINT_TO_POINTER (index));
+        if (stream == NULL)
+                return;
+
+        device = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+        if (device != NULL) {
+                gvc_mixer_ui_device_invalidate_stream (device);
+                if (!gvc_mixer_ui_device_has_ports (device)) {
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[OUTPUT_REMOVED],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (device));
+                } else {
+                        GList *devices, *d;
+
+                        devices = g_hash_table_get_values (control->priv->ui_outputs);
+
+                        for (d = devices; d != NULL; d = d->next) {
+                                guint stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+                                device = d->data;
+                                g_object_get (G_OBJECT (device),
+                                             "stream-id", &stream_id,
+                                              NULL);
+                                if (stream_id == gvc_mixer_stream_get_id (stream))
+                                        gvc_mixer_ui_device_invalidate_stream (device);
+                        }
+
+                        g_list_free (devices);
+                }
+        }
+
+        g_hash_table_remove (control->priv->sinks,
+                             GUINT_TO_POINTER (index));
+
+        remove_stream (control, stream);
+}
+
+static void
+remove_source (GvcMixerControl *control,
+               guint            index)
+{
+        GvcMixerStream   *stream;
+        GvcMixerUIDevice *device;
+
+        g_debug ("Removing source: index=%u", index);
+
+        stream = g_hash_table_lookup (control->priv->sources,
+                                      GUINT_TO_POINTER (index));
+        if (stream == NULL)
+                return;
+
+        device = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+        if (device != NULL) {
+                gvc_mixer_ui_device_invalidate_stream (device);
+                if (!gvc_mixer_ui_device_has_ports (device)) {
+                        g_signal_emit (G_OBJECT (control),
+                                       signals[INPUT_REMOVED],
+                                       0,
+                                       gvc_mixer_ui_device_get_id (device));
+                } else {
+                        GList *devices, *d;
+
+                        devices = g_hash_table_get_values (control->priv->ui_inputs);
+
+                        for (d = devices; d != NULL; d = d->next) {
+                                guint stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+                                device = d->data;
+                                g_object_get (G_OBJECT (device),
+                                             "stream-id", &stream_id,
+                                              NULL);
+                                if (stream_id == gvc_mixer_stream_get_id (stream))
+                                        gvc_mixer_ui_device_invalidate_stream (device);
+                        }
+
+                        g_list_free (devices);
+                }
+        }
+
+        g_hash_table_remove (control->priv->sources,
+                             GUINT_TO_POINTER (index));
+
+        remove_stream (control, stream);
+}
+
+static void
+remove_sink_input (GvcMixerControl *control,
+                   guint            index)
+{
+        GvcMixerStream *stream;
+
+        g_debug ("Removing sink input: index=%u", index);
+
+        stream = g_hash_table_lookup (control->priv->sink_inputs,
+                                      GUINT_TO_POINTER (index));
+        if (stream == NULL) {
+                return;
+        }
+        g_hash_table_remove (control->priv->sink_inputs,
+                             GUINT_TO_POINTER (index));
+
+        remove_stream (control, stream);
+}
+
+static void
+remove_source_output (GvcMixerControl *control,
+                      guint            index)
+{
+        GvcMixerStream *stream;
+
+        g_debug ("Removing source output: index=%u", index);
+
+        stream = g_hash_table_lookup (control->priv->source_outputs,
+                                      GUINT_TO_POINTER (index));
+        if (stream == NULL) {
+                return;
+        }
+        g_hash_table_remove (control->priv->source_outputs,
+                             GUINT_TO_POINTER (index));
+
+        remove_stream (control, stream);
+}
+
+static void
+_pa_context_subscribe_cb (pa_context                  *context,
+                          pa_subscription_event_type_t t,
+                          uint32_t                     index,
+                          void                        *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
+        case PA_SUBSCRIPTION_EVENT_SINK:
+                if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+                        remove_sink (control, index);
+                } else {
+                        req_update_sink_info (control, index);
+                }
+                break;
+
+        case PA_SUBSCRIPTION_EVENT_SOURCE:
+                if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+                        remove_source (control, index);
+                } else {
+                        req_update_source_info (control, index);
+                }
+                break;
+
+        case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
+                if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+                        remove_sink_input (control, index);
+                } else {
+                        req_update_sink_input_info (control, index);
+                }
+                break;
+
+        case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
+                if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+                        remove_source_output (control, index);
+                } else {
+                        req_update_source_output_info (control, index);
+                }
+                break;
+
+        case PA_SUBSCRIPTION_EVENT_CLIENT:
+                if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+                        remove_client (control, index);
+                } else {
+                        req_update_client_info (control, index);
+                }
+                break;
+
+        case PA_SUBSCRIPTION_EVENT_SERVER:
+                req_update_server_info (control, index);
+                break;
+
+        case PA_SUBSCRIPTION_EVENT_CARD:
+                if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+                        remove_card (control, index);
+                } else {
+                        req_update_card (control, index);
+                }
+                break;
+        default:
+                break;
+        }
+}
+
+static void
+gvc_mixer_control_ready (GvcMixerControl *control)
+{
+        pa_operation *o;
+
+        pa_context_set_subscribe_callback (control->priv->pa_context,
+                                           _pa_context_subscribe_cb,
+                                           control);
+        o = pa_context_subscribe (control->priv->pa_context,
+                                  (pa_subscription_mask_t)
+                                  (PA_SUBSCRIPTION_MASK_SINK|
+                                   PA_SUBSCRIPTION_MASK_SOURCE|
+                                   PA_SUBSCRIPTION_MASK_SINK_INPUT|
+                                   PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT|
+                                   PA_SUBSCRIPTION_MASK_CLIENT|
+                                   PA_SUBSCRIPTION_MASK_SERVER|
+                                   PA_SUBSCRIPTION_MASK_CARD),
+                                  NULL,
+                                  NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_subscribe() failed");
+                return;
+        }
+        pa_operation_unref (o);
+
+        req_update_server_info (control, -1);
+        req_update_card (control, -1);
+        req_update_client_info (control, -1);
+        req_update_sink_info (control, -1);
+        req_update_source_info (control, -1);
+        req_update_sink_input_info (control, -1);
+        req_update_source_output_info (control, -1);
+
+        control->priv->server_protocol_version = pa_context_get_server_protocol_version (control->priv->pa_context);
+
+        control->priv->n_outstanding = 6;
+
+        /* This call is not always supported */
+        o = pa_ext_stream_restore_read (control->priv->pa_context,
+                                        _pa_ext_stream_restore_read_cb,
+                                        control);
+        if (o != NULL) {
+                pa_operation_unref (o);
+                control->priv->n_outstanding++;
+
+                pa_ext_stream_restore_set_subscribe_cb (control->priv->pa_context,
+                                                        _pa_ext_stream_restore_subscribe_cb,
+                                                        control);
+
+                o = pa_ext_stream_restore_subscribe (control->priv->pa_context,
+                                                     1,
+                                                     NULL,
+                                                     NULL);
+                if (o != NULL) {
+                        pa_operation_unref (o);
+                }
+
+        } else {
+                g_debug ("Failed to initialized stream_restore extension: %s",
+                         pa_strerror (pa_context_errno (control->priv->pa_context)));
+        }
+}
+
+static void
+gvc_mixer_new_pa_context (GvcMixerControl *self)
+{
+        pa_proplist     *proplist;
+
+        g_return_if_fail (self);
+        g_return_if_fail (!self->priv->pa_context);
+
+        proplist = pa_proplist_new ();
+        pa_proplist_sets (proplist,
+                          PA_PROP_APPLICATION_NAME,
+                          self->priv->name);
+        pa_proplist_sets (proplist,
+                          PA_PROP_APPLICATION_ID,
+                          "org.gnome.VolumeControl");
+        pa_proplist_sets (proplist,
+                          PA_PROP_APPLICATION_ICON_NAME,
+                          "multimedia-volume-control");
+        pa_proplist_sets (proplist,
+                          PA_PROP_APPLICATION_VERSION,
+                          PACKAGE_VERSION);
+
+        self->priv->pa_context = pa_context_new_with_proplist (self->priv->pa_api, NULL, proplist);
+
+        pa_proplist_free (proplist);
+        g_assert (self->priv->pa_context);
+}
+
+static void
+remove_all_items (GvcMixerControl *control,
+                  GHashTable *hash_table,
+                  void (*remove_item)(GvcMixerControl *control, guint index))
+{
+        GHashTableIter iter;
+        gpointer key, value;
+
+        g_hash_table_iter_init (&iter, hash_table);
+        while (g_hash_table_iter_next (&iter, &key, &value)) {
+                if (remove_item) {
+                        remove_item (control, GPOINTER_TO_UINT (key));
+                        g_hash_table_remove (hash_table, key);
+                        g_hash_table_iter_init (&iter, hash_table);
+                } else {
+                        g_hash_table_iter_remove (&iter);
+                }
+        }
+}
+
+static gboolean
+idle_reconnect (gpointer data)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (data);
+
+        g_return_val_if_fail (control, FALSE);
+
+        g_debug ("Reconnect: clean up all objects");
+
+        remove_all_items (control, control->priv->sinks, remove_sink);
+        remove_all_items (control, control->priv->sources, remove_source);
+        remove_all_items (control, control->priv->sink_inputs, remove_sink_input);
+        remove_all_items (control, control->priv->source_outputs, remove_source_output);
+        remove_all_items (control, control->priv->cards, remove_card);
+        remove_all_items (control, control->priv->ui_inputs, NULL);
+        remove_all_items (control, control->priv->ui_outputs, NULL);
+        remove_all_items (control, control->priv->clients, remove_client);
+
+        g_debug ("Reconnect: make new connection");
+
+        if (control->priv->pa_context) {
+                pa_context_unref (control->priv->pa_context);
+                control->priv->pa_context = NULL;
+                control->priv->server_protocol_version = 0;
+                gvc_mixer_new_pa_context (control);
+        }
+
+        gvc_mixer_control_open (control); /* cannot fail */
+
+        control->priv->reconnect_id = 0;
+        return FALSE;
+}
+
+static void
+_pa_context_state_cb (pa_context *context,
+                      void       *userdata)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+        switch (pa_context_get_state (context)) {
+        case PA_CONTEXT_UNCONNECTED:
+        case PA_CONTEXT_CONNECTING:
+        case PA_CONTEXT_AUTHORIZING:
+        case PA_CONTEXT_SETTING_NAME:
+                break;
+
+        case PA_CONTEXT_READY:
+                gvc_mixer_control_ready (control);
+                break;
+
+        case PA_CONTEXT_FAILED:
+                control->priv->state = GVC_STATE_FAILED;
+                g_signal_emit (control, signals[STATE_CHANGED], 0, GVC_STATE_FAILED);
+                if (control->priv->reconnect_id == 0)
+                        control->priv->reconnect_id = g_timeout_add_seconds (RECONNECT_DELAY, idle_reconnect, control);
+                break;
+
+        case PA_CONTEXT_TERMINATED:
+        default:
+                /* FIXME: */
+                break;
+        }
+}
+
+gboolean
+gvc_mixer_control_open (GvcMixerControl *control)
+{
+        int res;
+
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+        g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
+        g_return_val_if_fail (pa_context_get_state (control->priv->pa_context) == PA_CONTEXT_UNCONNECTED, FALSE);
+
+        pa_context_set_state_callback (control->priv->pa_context,
+                                       _pa_context_state_cb,
+                                       control);
+
+        control->priv->state = GVC_STATE_CONNECTING;
+        g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CONNECTING);
+        res = pa_context_connect (control->priv->pa_context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFAIL, NULL);
+        if (res < 0) {
+                g_warning ("Failed to connect context: %s",
+                           pa_strerror (pa_context_errno (control->priv->pa_context)));
+        }
+
+        return res;
+}
+
+gboolean
+gvc_mixer_control_close (GvcMixerControl *control)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+        g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
+
+        pa_context_disconnect (control->priv->pa_context);
+
+        control->priv->state = GVC_STATE_CLOSED;
+        g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CLOSED);
+        return TRUE;
+}
+
+static void
+gvc_mixer_control_dispose (GObject *object)
+{
+        GvcMixerControl *control = GVC_MIXER_CONTROL (object);
+
+        if (control->priv->reconnect_id != 0) {
+                g_source_remove (control->priv->reconnect_id);
+                control->priv->reconnect_id = 0;
+        }
+
+        if (control->priv->pa_context != NULL) {
+                pa_context_unref (control->priv->pa_context);
+                control->priv->pa_context = NULL;
+        }
+
+        if (control->priv->default_source_name != NULL) {
+                g_free (control->priv->default_source_name);
+                control->priv->default_source_name = NULL;
+        }
+        if (control->priv->default_sink_name != NULL) {
+                g_free (control->priv->default_sink_name);
+                control->priv->default_sink_name = NULL;
+        }
+
+        if (control->priv->pa_mainloop != NULL) {
+                pa_glib_mainloop_free (control->priv->pa_mainloop);
+                control->priv->pa_mainloop = NULL;
+        }
+
+        if (control->priv->all_streams != NULL) {
+                g_hash_table_destroy (control->priv->all_streams);
+                control->priv->all_streams = NULL;
+        }
+
+        if (control->priv->sinks != NULL) {
+                g_hash_table_destroy (control->priv->sinks);
+                control->priv->sinks = NULL;
+        }
+        if (control->priv->sources != NULL) {
+                g_hash_table_destroy (control->priv->sources);
+                control->priv->sources = NULL;
+        }
+        if (control->priv->sink_inputs != NULL) {
+                g_hash_table_destroy (control->priv->sink_inputs);
+                control->priv->sink_inputs = NULL;
+        }
+        if (control->priv->source_outputs != NULL) {
+                g_hash_table_destroy (control->priv->source_outputs);
+                control->priv->source_outputs = NULL;
+        }
+        if (control->priv->clients != NULL) {
+                g_hash_table_destroy (control->priv->clients);
+                control->priv->clients = NULL;
+        }
+        if (control->priv->cards != NULL) {
+                g_hash_table_destroy (control->priv->cards);
+                control->priv->cards = NULL;
+        }
+        if (control->priv->ui_outputs != NULL) {
+                g_hash_table_destroy (control->priv->ui_outputs);
+                control->priv->ui_outputs = NULL;
+        }
+        if (control->priv->ui_inputs != NULL) {
+                g_hash_table_destroy (control->priv->ui_inputs);
+                control->priv->ui_inputs = NULL;
+        }
+
+        free_priv_port_names (control);
+        G_OBJECT_CLASS (gvc_mixer_control_parent_class)->dispose (object);
+}
+
+static void
+gvc_mixer_control_set_property (GObject       *object,
+                                guint          prop_id,
+                                const GValue  *value,
+                                GParamSpec    *pspec)
+{
+        GvcMixerControl *self = GVC_MIXER_CONTROL (object);
+
+        switch (prop_id) {
+        case PROP_NAME:
+                g_free (self->priv->name);
+                self->priv->name = g_value_dup_string (value);
+                g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_NAME]);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gvc_mixer_control_get_property (GObject     *object,
+                                guint        prop_id,
+                                GValue      *value,
+                                GParamSpec  *pspec)
+{
+        GvcMixerControl *self = GVC_MIXER_CONTROL (object);
+
+        switch (prop_id) {
+        case PROP_NAME:
+                g_value_set_string (value, self->priv->name);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+
+static GObject *
+gvc_mixer_control_constructor (GType                  type,
+                               guint                  n_construct_properties,
+                               GObjectConstructParam *construct_params)
+{
+        GObject         *object;
+        GvcMixerControl *self;
+
+        object = G_OBJECT_CLASS (gvc_mixer_control_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+        self = GVC_MIXER_CONTROL (object);
+
+        gvc_mixer_new_pa_context (self);
+        self->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
+
+        return object;
+}
+
+static void
+gvc_mixer_control_class_init (GvcMixerControlClass *klass)
+{
+        GObjectClass   *object_class = G_OBJECT_CLASS (klass);
+
+        object_class->constructor = gvc_mixer_control_constructor;
+        object_class->dispose = gvc_mixer_control_dispose;
+        object_class->finalize = gvc_mixer_control_finalize;
+        object_class->set_property = gvc_mixer_control_set_property;
+        object_class->get_property = gvc_mixer_control_get_property;
+
+        obj_props[PROP_NAME] = g_param_spec_string ("name",
+                                                    "Name",
+                                                    "Name to display for this mixer control",
+                                                    NULL,
+                                                    G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        g_object_class_install_properties (object_class, N_PROPS, obj_props);
+
+        signals [STATE_CHANGED] =
+                g_signal_new ("state-changed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, state_changed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [STREAM_ADDED] =
+                g_signal_new ("stream-added",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, stream_added),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [STREAM_REMOVED] =
+                g_signal_new ("stream-removed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, stream_removed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [STREAM_CHANGED] =
+                g_signal_new ("stream-changed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, stream_changed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [AUDIO_DEVICE_SELECTION_NEEDED] =
+                g_signal_new ("audio-device-selection-needed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              0,
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_UINT);
+        signals [CARD_ADDED] =
+                g_signal_new ("card-added",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, card_added),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [CARD_REMOVED] =
+                g_signal_new ("card-removed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, card_removed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [DEFAULT_SINK_CHANGED] =
+                g_signal_new ("default-sink-changed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, default_sink_changed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [DEFAULT_SOURCE_CHANGED] =
+                g_signal_new ("default-source-changed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, default_source_changed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [ACTIVE_OUTPUT_UPDATE] =
+                g_signal_new ("active-output-update",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, active_output_update),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [ACTIVE_INPUT_UPDATE] =
+                g_signal_new ("active-input-update",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, active_input_update),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [OUTPUT_ADDED] =
+                g_signal_new ("output-added",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, output_added),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [INPUT_ADDED] =
+                g_signal_new ("input-added",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, input_added),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [OUTPUT_REMOVED] =
+                g_signal_new ("output-removed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, output_removed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+        signals [INPUT_REMOVED] =
+                g_signal_new ("input-removed",
+                              G_TYPE_FROM_CLASS (klass),
+                              G_SIGNAL_RUN_LAST,
+                              G_STRUCT_OFFSET (GvcMixerControlClass, input_removed),
+                              NULL, NULL, NULL,
+                              G_TYPE_NONE, 1, G_TYPE_UINT);
+}
+
+
+static void
+gvc_mixer_control_init (GvcMixerControl *control)
+{
+        control->priv = gvc_mixer_control_get_instance_private (control);
+
+        control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ());
+        g_assert (control->priv->pa_mainloop);
+
+        control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop);
+        g_assert (control->priv->pa_api);
+
+        control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+        control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+
+        control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free);
+
+#ifdef HAVE_ALSA
+        control->priv->headset_card = -1;
+#endif /* HAVE_ALSA */
+
+        control->priv->state = GVC_STATE_CLOSED;
+}
+
+static void
+gvc_mixer_control_finalize (GObject *object)
+{
+        GvcMixerControl *mixer_control;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_CONTROL (object));
+
+        mixer_control = GVC_MIXER_CONTROL (object);
+        g_free (mixer_control->priv->name);
+        mixer_control->priv->name = NULL;
+
+        g_return_if_fail (mixer_control->priv != NULL);
+        G_OBJECT_CLASS (gvc_mixer_control_parent_class)->finalize (object);
+}
+
+GvcMixerControl *
+gvc_mixer_control_new (const char *name)
+{
+        GObject *control;
+        control = g_object_new (GVC_TYPE_MIXER_CONTROL,
+                                "name", name,
+                                NULL);
+        return GVC_MIXER_CONTROL (control);
+}
+
+gdouble
+gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control)
+{
+	g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0);
+	return (gdouble) PA_VOLUME_NORM;
+}
+
+gdouble
+gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control)
+{
+	g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0);
+	return (gdouble) PA_VOLUME_UI_MAX;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-control.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CONTROL_H
+#define __GVC_MIXER_CONTROL_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+#include "gvc-mixer-card.h"
+#include "gvc-mixer-ui-device.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+        GVC_STATE_CLOSED,
+        GVC_STATE_READY,
+        GVC_STATE_CONNECTING,
+        GVC_STATE_FAILED
+} GvcMixerControlState;
+
+typedef enum
+{
+        GVC_HEADSET_PORT_CHOICE_NONE        = 0,
+        GVC_HEADSET_PORT_CHOICE_HEADPHONES  = 1 << 0,
+        GVC_HEADSET_PORT_CHOICE_HEADSET     = 1 << 1,
+        GVC_HEADSET_PORT_CHOICE_MIC         = 1 << 2
+} GvcHeadsetPortChoice;
+
+#define GVC_TYPE_MIXER_CONTROL         (gvc_mixer_control_get_type ())
+#define GVC_MIXER_CONTROL(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControl))
+#define GVC_MIXER_CONTROL_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass))
+#define GVC_IS_MIXER_CONTROL(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CONTROL))
+#define GVC_IS_MIXER_CONTROL_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CONTROL))
+#define GVC_MIXER_CONTROL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass))
+
+typedef struct GvcMixerControlPrivate GvcMixerControlPrivate;
+
+typedef struct
+{
+        GObject                 parent;
+        GvcMixerControlPrivate *priv;
+} GvcMixerControl;
+
+typedef struct
+{
+        GObjectClass            parent_class;
+
+        void (*state_changed)          (GvcMixerControl      *control,
+                                        GvcMixerControlState  new_state);
+        void (*stream_added)           (GvcMixerControl *control,
+                                        guint            id);
+        void (*stream_changed)         (GvcMixerControl *control,
+                                        guint            id);
+        void (*stream_removed)         (GvcMixerControl *control,
+                                        guint            id);
+        void (*card_added)             (GvcMixerControl *control,
+                                        guint            id);
+        void (*card_removed)           (GvcMixerControl *control,
+                                        guint            id);
+        void (*default_sink_changed)   (GvcMixerControl *control,
+                                        guint            id);
+        void (*default_source_changed) (GvcMixerControl *control,
+                                        guint            id);
+        void (*active_output_update)   (GvcMixerControl *control,
+                                        guint            id);
+        void (*active_input_update)    (GvcMixerControl *control,
+                                        guint            id);
+        void (*output_added)           (GvcMixerControl *control,
+                                        guint            id);
+        void (*input_added)            (GvcMixerControl *control,
+                                        guint            id);
+        void (*output_removed)         (GvcMixerControl *control,
+                                        guint            id);
+        void (*input_removed)          (GvcMixerControl *control,
+                                        guint            id);
+        void (*audio_device_selection_needed)
+                                       (GvcMixerControl      *control,
+                                        guint                 id,
+                                        gboolean              show_dialog,
+                                        GvcHeadsetPortChoice  choices);
+} GvcMixerControlClass;
+
+GType               gvc_mixer_control_get_type            (void);
+
+GvcMixerControl *   gvc_mixer_control_new                 (const char *name);
+
+gboolean            gvc_mixer_control_open                (GvcMixerControl *control);
+gboolean            gvc_mixer_control_close               (GvcMixerControl *control);
+
+GSList *            gvc_mixer_control_get_cards           (GvcMixerControl *control);
+GSList *            gvc_mixer_control_get_streams         (GvcMixerControl *control);
+GSList *            gvc_mixer_control_get_sinks           (GvcMixerControl *control);
+GSList *            gvc_mixer_control_get_sources         (GvcMixerControl *control);
+GSList *            gvc_mixer_control_get_sink_inputs     (GvcMixerControl *control);
+GSList *            gvc_mixer_control_get_source_outputs  (GvcMixerControl *control);
+
+GvcMixerStream *        gvc_mixer_control_lookup_stream_id          (GvcMixerControl *control,
+                                                                     guint            id);
+GvcMixerCard   *        gvc_mixer_control_lookup_card_id            (GvcMixerControl *control,
+                                                                     guint            id);
+GvcMixerUIDevice *      gvc_mixer_control_lookup_output_id          (GvcMixerControl *control,
+                                                                     guint            id);
+GvcMixerUIDevice *      gvc_mixer_control_lookup_input_id           (GvcMixerControl *control,
+                                                                     guint            id);
+GvcMixerUIDevice *      gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control,
+                                                                     GvcMixerStream *stream);
+
+GvcMixerStream *        gvc_mixer_control_get_default_sink     (GvcMixerControl *control);
+GvcMixerStream *        gvc_mixer_control_get_default_source   (GvcMixerControl *control);
+GvcMixerStream *        gvc_mixer_control_get_event_sink_input (GvcMixerControl *control);
+
+gboolean                gvc_mixer_control_set_default_sink     (GvcMixerControl *control,
+                                                                GvcMixerStream  *stream);
+gboolean                gvc_mixer_control_set_default_source   (GvcMixerControl *control,
+                                                                GvcMixerStream  *stream);
+
+gdouble                 gvc_mixer_control_get_vol_max_norm                  (GvcMixerControl *control);
+gdouble                 gvc_mixer_control_get_vol_max_amplified             (GvcMixerControl *control);
+void                    gvc_mixer_control_change_output                     (GvcMixerControl *control,
+                                                                             GvcMixerUIDevice* output);
+void                    gvc_mixer_control_change_input                      (GvcMixerControl *control,
+                                                                             GvcMixerUIDevice* input);
+GvcMixerStream*         gvc_mixer_control_get_stream_from_device            (GvcMixerControl *control,
+                                                                             GvcMixerUIDevice *device);
+gboolean                gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control,
+                                                                             GvcMixerUIDevice *device,
+                                                                             const gchar* profile);
+
+void                    gvc_mixer_control_set_headset_port                  (GvcMixerControl      *control,
+                                                                             guint                 id,
+                                                                             GvcHeadsetPortChoice  choices);
+
+GvcMixerControlState gvc_mixer_control_get_state            (GvcMixerControl *control);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CONTROL_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-event-role.c
@@ -0,0 +1,229 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/ext-stream-restore.h>
+
+#include "gvc-mixer-event-role.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerEventRolePrivate
+{
+        char          *device;
+};
+
+enum
+{
+        PROP_0,
+        PROP_DEVICE,
+        N_PROPS
+};
+static GParamSpec *obj_props[N_PROPS] = { NULL, };
+
+static void     gvc_mixer_event_role_finalize   (GObject            *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerEventRole, gvc_mixer_event_role, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+update_settings (GvcMixerEventRole *role,
+                 gboolean           is_muted,
+                 gpointer          *op)
+{
+        pa_operation              *o;
+        const GvcChannelMap       *map;
+        pa_context                *context;
+        pa_ext_stream_restore_info info;
+
+        map = gvc_mixer_stream_get_channel_map (GVC_MIXER_STREAM(role));
+
+        info.volume = *gvc_channel_map_get_cvolume(map);
+        info.name = "sink-input-by-media-role:event";
+        info.channel_map = *gvc_channel_map_get_pa_channel_map(map);
+        info.device = role->priv->device;
+        info.mute = is_muted;
+
+        context = gvc_mixer_stream_get_pa_context (GVC_MIXER_STREAM (role));
+
+        o = pa_ext_stream_restore_write (context,
+                                         PA_UPDATE_REPLACE,
+                                         &info,
+                                         1,
+                                         TRUE,
+                                         NULL,
+                                         NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_ext_stream_restore_write() failed");
+                return FALSE;
+        }
+
+        if (op != NULL)
+                *op = o;
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_event_role_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+        return update_settings (GVC_MIXER_EVENT_ROLE (stream),
+                                gvc_mixer_stream_get_is_muted (stream), op);
+}
+
+static gboolean
+gvc_mixer_event_role_change_is_muted (GvcMixerStream *stream,
+                                      gboolean        is_muted)
+{
+        /* Apply change straight away so that we don't get a race with
+         * gvc_mixer_event_role_push_volume().
+         * See https://bugs.freedesktop.org/show_bug.cgi?id=51413 */
+        gvc_mixer_stream_set_is_muted (stream, is_muted);
+        return update_settings (GVC_MIXER_EVENT_ROLE (stream),
+                                is_muted, NULL);
+}
+
+static gboolean
+gvc_mixer_event_role_set_device (GvcMixerEventRole *role,
+                                 const char        *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_EVENT_ROLE (role), FALSE);
+
+        g_free (role->priv->device);
+        role->priv->device = g_strdup (device);
+        g_object_notify_by_pspec (G_OBJECT (role), obj_props[PROP_DEVICE]);
+
+        return TRUE;
+}
+
+static void
+gvc_mixer_event_role_set_property (GObject       *object,
+                                   guint          prop_id,
+                                   const GValue  *value,
+                                   GParamSpec    *pspec)
+{
+        GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object);
+
+        switch (prop_id) {
+        case PROP_DEVICE:
+                gvc_mixer_event_role_set_device (self, g_value_get_string (value));
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gvc_mixer_event_role_get_property (GObject     *object,
+                                   guint        prop_id,
+                                   GValue      *value,
+                                   GParamSpec  *pspec)
+{
+        GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object);
+
+        switch (prop_id) {
+        case PROP_DEVICE:
+                g_value_set_string (value, self->priv->device);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gvc_mixer_event_role_class_init (GvcMixerEventRoleClass *klass)
+{
+        GObjectClass        *object_class = G_OBJECT_CLASS (klass);
+        GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+        object_class->finalize = gvc_mixer_event_role_finalize;
+        object_class->set_property = gvc_mixer_event_role_set_property;
+        object_class->get_property = gvc_mixer_event_role_get_property;
+
+        stream_class->push_volume = gvc_mixer_event_role_push_volume;
+        stream_class->change_is_muted = gvc_mixer_event_role_change_is_muted;
+
+        obj_props[PROP_DEVICE] = g_param_spec_string ("device",
+                                                      "Device",
+                                                      "Device",
+                                                      NULL,
+                                                      G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        g_object_class_install_properties (object_class, N_PROPS, obj_props);
+}
+
+static void
+gvc_mixer_event_role_init (GvcMixerEventRole *event_role)
+{
+        event_role->priv = gvc_mixer_event_role_get_instance_private (event_role);
+
+}
+
+static void
+gvc_mixer_event_role_finalize (GObject *object)
+{
+        GvcMixerEventRole *mixer_event_role;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_EVENT_ROLE (object));
+
+        mixer_event_role = GVC_MIXER_EVENT_ROLE (object);
+
+        g_return_if_fail (mixer_event_role->priv != NULL);
+
+        g_free (mixer_event_role->priv->device);
+
+        G_OBJECT_CLASS (gvc_mixer_event_role_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_event_role_new: (skip)
+ * @context:
+ * @device:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_event_role_new (pa_context *context,
+                          const char *device,
+                          GvcChannelMap *channel_map)
+{
+        GObject *object;
+
+        object = g_object_new (GVC_TYPE_MIXER_EVENT_ROLE,
+                               "pa-context", context,
+                               "index", 0,
+                               "device", device,
+                               "channel-map", channel_map,
+                               NULL);
+
+        return GVC_MIXER_STREAM (object);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-event-role.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_EVENT_ROLE_H
+#define __GVC_MIXER_EVENT_ROLE_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_EVENT_ROLE         (gvc_mixer_event_role_get_type ())
+#define GVC_MIXER_EVENT_ROLE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRole))
+#define GVC_MIXER_EVENT_ROLE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass))
+#define GVC_IS_MIXER_EVENT_ROLE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_EVENT_ROLE))
+#define GVC_IS_MIXER_EVENT_ROLE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_EVENT_ROLE))
+#define GVC_MIXER_EVENT_ROLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass))
+
+typedef struct GvcMixerEventRolePrivate GvcMixerEventRolePrivate;
+
+typedef struct
+{
+        GvcMixerStream            parent;
+        GvcMixerEventRolePrivate *priv;
+} GvcMixerEventRole;
+
+typedef struct
+{
+        GvcMixerStreamClass parent_class;
+} GvcMixerEventRoleClass;
+
+GType               gvc_mixer_event_role_get_type      (void);
+
+GvcMixerStream *    gvc_mixer_event_role_new           (pa_context    *context,
+                                                        const char    *device,
+                                                        GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_EVENT_ROLE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-sink-input.c
@@ -0,0 +1,159 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-sink-input.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSinkInputPrivate
+{
+        gpointer dummy;
+};
+
+static void     gvc_mixer_sink_input_finalize   (GObject                *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSinkInput, gvc_mixer_sink_input, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_sink_input_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+        pa_operation        *o;
+        guint                index;
+        const GvcChannelMap *map;
+        pa_context          *context;
+        const pa_cvolume    *cv;
+
+        index = gvc_mixer_stream_get_index (stream);
+
+        map = gvc_mixer_stream_get_channel_map (stream);
+
+        cv = gvc_channel_map_get_cvolume(map);
+
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_sink_input_volume (context,
+                                              index,
+                                              cv,
+                                              NULL,
+                                              NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_sink_input_volume() failed");
+                return FALSE;
+        }
+
+        *op = o;
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_sink_input_change_is_muted (GvcMixerStream *stream,
+                                      gboolean        is_muted)
+{
+        pa_operation *o;
+        guint         index;
+        pa_context   *context;
+
+        index = gvc_mixer_stream_get_index (stream);
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_sink_input_mute (context,
+                                            index,
+                                            is_muted,
+                                            NULL,
+                                            NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_sink_input_mute_by_index() failed");
+                return FALSE;
+        }
+
+        pa_operation_unref(o);
+
+        return TRUE;
+}
+
+static void
+gvc_mixer_sink_input_class_init (GvcMixerSinkInputClass *klass)
+{
+        GObjectClass        *object_class = G_OBJECT_CLASS (klass);
+        GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+        object_class->finalize = gvc_mixer_sink_input_finalize;
+
+        stream_class->push_volume = gvc_mixer_sink_input_push_volume;
+        stream_class->change_is_muted = gvc_mixer_sink_input_change_is_muted;
+}
+
+static void
+gvc_mixer_sink_input_init (GvcMixerSinkInput *sink_input)
+{
+        sink_input->priv = gvc_mixer_sink_input_get_instance_private (sink_input);
+}
+
+static void
+gvc_mixer_sink_input_finalize (GObject *object)
+{
+        GvcMixerSinkInput *mixer_sink_input;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_SINK_INPUT (object));
+
+        mixer_sink_input = GVC_MIXER_SINK_INPUT (object);
+
+        g_return_if_fail (mixer_sink_input->priv != NULL);
+        G_OBJECT_CLASS (gvc_mixer_sink_input_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_sink_input_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_sink_input_new (pa_context    *context,
+                          guint          index,
+                          GvcChannelMap *channel_map)
+{
+        GObject *object;
+
+        object = g_object_new (GVC_TYPE_MIXER_SINK_INPUT,
+                               "pa-context", context,
+                               "index", index,
+                               "channel-map", channel_map,
+                               NULL);
+
+        return GVC_MIXER_STREAM (object);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-sink-input.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SINK_INPUT_H
+#define __GVC_MIXER_SINK_INPUT_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SINK_INPUT         (gvc_mixer_sink_input_get_type ())
+#define GVC_MIXER_SINK_INPUT(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInput))
+#define GVC_MIXER_SINK_INPUT_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass))
+#define GVC_IS_MIXER_SINK_INPUT(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK_INPUT))
+#define GVC_IS_MIXER_SINK_INPUT_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK_INPUT))
+#define GVC_MIXER_SINK_INPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass))
+
+typedef struct GvcMixerSinkInputPrivate GvcMixerSinkInputPrivate;
+
+typedef struct
+{
+        GvcMixerStream            parent;
+        GvcMixerSinkInputPrivate *priv;
+} GvcMixerSinkInput;
+
+typedef struct
+{
+        GvcMixerStreamClass parent_class;
+} GvcMixerSinkInputClass;
+
+GType               gvc_mixer_sink_input_get_type      (void);
+
+GvcMixerStream *    gvc_mixer_sink_input_new           (pa_context    *context,
+                                                        guint          index,
+                                                        GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SINK_INPUT_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-sink.c
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-sink.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSinkPrivate
+{
+        gpointer dummy;
+};
+
+static void     gvc_mixer_sink_finalize   (GObject           *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSink, gvc_mixer_sink, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_sink_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+        pa_operation        *o;
+        guint                index;
+        const GvcChannelMap *map;
+        pa_context          *context;
+        const pa_cvolume    *cv;
+
+        index = gvc_mixer_stream_get_index (stream);
+
+        map = gvc_mixer_stream_get_channel_map (stream);
+
+        /* set the volume */
+        cv = gvc_channel_map_get_cvolume(map);
+
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_sink_volume_by_index (context,
+                                                 index,
+                                                 cv,
+                                                 NULL,
+                                                 NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_sink_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+                return FALSE;
+        }
+
+        *op = o;
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_sink_change_is_muted (GvcMixerStream *stream,
+                                gboolean        is_muted)
+{
+        pa_operation *o;
+        guint         index;
+        pa_context   *context;
+
+        index = gvc_mixer_stream_get_index (stream);
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_sink_mute_by_index (context,
+                                               index,
+                                               is_muted,
+                                               NULL,
+                                               NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_sink_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+                return FALSE;
+        }
+
+        pa_operation_unref(o);
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_sink_change_port (GvcMixerStream *stream,
+                            const char     *port)
+{
+        pa_operation *o;
+        guint         index;
+        pa_context   *context;
+
+        index = gvc_mixer_stream_get_index (stream);
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_sink_port_by_index (context,
+                                               index,
+                                               port,
+                                               NULL,
+                                               NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_sink_port_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+                return FALSE;
+        }
+
+        pa_operation_unref(o);
+
+        return TRUE;
+}
+
+static void
+gvc_mixer_sink_class_init (GvcMixerSinkClass *klass)
+{
+        GObjectClass        *object_class = G_OBJECT_CLASS (klass);
+        GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+        object_class->finalize = gvc_mixer_sink_finalize;
+
+        stream_class->push_volume = gvc_mixer_sink_push_volume;
+        stream_class->change_port = gvc_mixer_sink_change_port;
+        stream_class->change_is_muted = gvc_mixer_sink_change_is_muted;
+}
+
+static void
+gvc_mixer_sink_init (GvcMixerSink *sink)
+{
+        sink->priv = gvc_mixer_sink_get_instance_private (sink);
+}
+
+static void
+gvc_mixer_sink_finalize (GObject *object)
+{
+        GvcMixerSink *mixer_sink;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_SINK (object));
+
+        mixer_sink = GVC_MIXER_SINK (object);
+
+        g_return_if_fail (mixer_sink->priv != NULL);
+        G_OBJECT_CLASS (gvc_mixer_sink_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_sink_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_sink_new (pa_context    *context,
+                    guint          index,
+                    GvcChannelMap *channel_map)
+
+{
+        GObject *object;
+
+        object = g_object_new (GVC_TYPE_MIXER_SINK,
+                               "pa-context", context,
+                               "index", index,
+                               "channel-map", channel_map,
+                               NULL);
+
+        return GVC_MIXER_STREAM (object);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-sink.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SINK_H
+#define __GVC_MIXER_SINK_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SINK         (gvc_mixer_sink_get_type ())
+#define GVC_MIXER_SINK(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK, GvcMixerSink))
+#define GVC_MIXER_SINK_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass))
+#define GVC_IS_MIXER_SINK(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK))
+#define GVC_IS_MIXER_SINK_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK))
+#define GVC_MIXER_SINK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass))
+
+typedef struct GvcMixerSinkPrivate GvcMixerSinkPrivate;
+
+typedef struct
+{
+        GvcMixerStream       parent;
+        GvcMixerSinkPrivate *priv;
+} GvcMixerSink;
+
+typedef struct
+{
+        GvcMixerStreamClass parent_class;
+} GvcMixerSinkClass;
+
+GType               gvc_mixer_sink_get_type            (void);
+
+GvcMixerStream *    gvc_mixer_sink_new                 (pa_context    *context,
+                                                        guint          index,
+                                                        GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SINK_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-source-output.c
@@ -0,0 +1,160 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-source-output.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSourceOutputPrivate
+{
+        gpointer dummy;
+};
+
+static void     gvc_mixer_source_output_finalize   (GObject            *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSourceOutput, gvc_mixer_source_output, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_source_output_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+        pa_operation        *o;
+        guint                index;
+        const GvcChannelMap *map;
+        pa_context          *context;
+        const pa_cvolume    *cv;
+
+        index = gvc_mixer_stream_get_index (stream);
+
+        map = gvc_mixer_stream_get_channel_map (stream);
+
+        cv = gvc_channel_map_get_cvolume(map);
+
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_source_output_volume (context,
+                                                 index,
+                                                 cv,
+                                                 NULL,
+                                                 NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_source_output_volume() failed");
+                return FALSE;
+        }
+
+        *op = o;
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_source_output_change_is_muted (GvcMixerStream *stream,
+                                      gboolean        is_muted)
+{
+        pa_operation *o;
+        guint         index;
+        pa_context   *context;
+
+        index = gvc_mixer_stream_get_index (stream);
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_source_output_mute (context,
+                                               index,
+                                               is_muted,
+                                               NULL,
+                                               NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_source_output_mute_by_index() failed");
+                return FALSE;
+        }
+
+        pa_operation_unref(o);
+
+        return TRUE;
+}
+
+static void
+gvc_mixer_source_output_class_init (GvcMixerSourceOutputClass *klass)
+{
+        GObjectClass        *object_class = G_OBJECT_CLASS (klass);
+        GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+        object_class->finalize = gvc_mixer_source_output_finalize;
+
+        stream_class->push_volume = gvc_mixer_source_output_push_volume;
+        stream_class->change_is_muted = gvc_mixer_source_output_change_is_muted;
+}
+
+static void
+gvc_mixer_source_output_init (GvcMixerSourceOutput *source_output)
+{
+        source_output->priv = gvc_mixer_source_output_get_instance_private (source_output);
+
+}
+
+static void
+gvc_mixer_source_output_finalize (GObject *object)
+{
+        GvcMixerSourceOutput *mixer_source_output;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_SOURCE_OUTPUT (object));
+
+        mixer_source_output = GVC_MIXER_SOURCE_OUTPUT (object);
+
+        g_return_if_fail (mixer_source_output->priv != NULL);
+        G_OBJECT_CLASS (gvc_mixer_source_output_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_source_output_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_source_output_new (pa_context    *context,
+                             guint          index,
+                             GvcChannelMap *channel_map)
+{
+        GObject *object;
+
+        object = g_object_new (GVC_TYPE_MIXER_SOURCE_OUTPUT,
+                               "pa-context", context,
+                               "index", index,
+                               "channel-map", channel_map,
+                               NULL);
+
+        return GVC_MIXER_STREAM (object);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-source-output.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SOURCE_OUTPUT_H
+#define __GVC_MIXER_SOURCE_OUTPUT_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SOURCE_OUTPUT         (gvc_mixer_source_output_get_type ())
+#define GVC_MIXER_SOURCE_OUTPUT(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutput))
+#define GVC_MIXER_SOURCE_OUTPUT_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass))
+#define GVC_IS_MIXER_SOURCE_OUTPUT(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT))
+#define GVC_IS_MIXER_SOURCE_OUTPUT_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE_OUTPUT))
+#define GVC_MIXER_SOURCE_OUTPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass))
+
+typedef struct GvcMixerSourceOutputPrivate GvcMixerSourceOutputPrivate;
+
+typedef struct
+{
+        GvcMixerStream               parent;
+        GvcMixerSourceOutputPrivate *priv;
+} GvcMixerSourceOutput;
+
+typedef struct
+{
+        GvcMixerStreamClass parent_class;
+} GvcMixerSourceOutputClass;
+
+GType               gvc_mixer_source_output_get_type      (void);
+
+GvcMixerStream *    gvc_mixer_source_output_new           (pa_context    *context,
+                                                           guint          index,
+                                                           GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SOURCE_OUTPUT_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-source.c
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-source.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSourcePrivate
+{
+        gpointer dummy;
+};
+
+static void     gvc_mixer_source_finalize   (GObject            *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSource, gvc_mixer_source, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_source_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+        pa_operation        *o;
+        guint                index;
+        const GvcChannelMap *map;
+        pa_context          *context;
+        const pa_cvolume    *cv;
+
+        index = gvc_mixer_stream_get_index (stream);
+
+        map = gvc_mixer_stream_get_channel_map (stream);
+
+        /* set the volume */
+        cv = gvc_channel_map_get_cvolume (map);
+
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_source_volume_by_index (context,
+                                                   index,
+                                                   cv,
+                                                   NULL,
+                                                   NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_source_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+                return FALSE;
+        }
+
+        *op = o;
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_source_change_is_muted (GvcMixerStream *stream,
+                                gboolean        is_muted)
+{
+        pa_operation *o;
+        guint         index;
+        pa_context   *context;
+
+        index = gvc_mixer_stream_get_index (stream);
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_source_mute_by_index (context,
+                                                 index,
+                                                 is_muted,
+                                                 NULL,
+                                                 NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_source_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+                return FALSE;
+        }
+
+        pa_operation_unref(o);
+
+        return TRUE;
+}
+
+static gboolean
+gvc_mixer_source_change_port (GvcMixerStream *stream,
+                              const char     *port)
+{
+        pa_operation *o;
+        guint         index;
+        pa_context   *context;
+
+        index = gvc_mixer_stream_get_index (stream);
+        context = gvc_mixer_stream_get_pa_context (stream);
+
+        o = pa_context_set_source_port_by_index (context,
+                                                 index,
+                                                 port,
+                                                 NULL,
+                                                 NULL);
+
+        if (o == NULL) {
+                g_warning ("pa_context_set_source_port_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+                return FALSE;
+        }
+
+        pa_operation_unref(o);
+
+        return TRUE;
+}
+
+static void
+gvc_mixer_source_class_init (GvcMixerSourceClass *klass)
+{
+        GObjectClass        *object_class = G_OBJECT_CLASS (klass);
+        GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+        object_class->finalize = gvc_mixer_source_finalize;
+
+        stream_class->push_volume = gvc_mixer_source_push_volume;
+        stream_class->change_is_muted = gvc_mixer_source_change_is_muted;
+        stream_class->change_port = gvc_mixer_source_change_port;
+}
+
+static void
+gvc_mixer_source_init (GvcMixerSource *source)
+{
+        source->priv = gvc_mixer_source_get_instance_private (source);
+}
+
+static void
+gvc_mixer_source_finalize (GObject *object)
+{
+        GvcMixerSource *mixer_source;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_SOURCE (object));
+
+        mixer_source = GVC_MIXER_SOURCE (object);
+
+        g_return_if_fail (mixer_source->priv != NULL);
+        G_OBJECT_CLASS (gvc_mixer_source_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_source_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_source_new (pa_context    *context,
+                      guint          index,
+                      GvcChannelMap *channel_map)
+
+{
+        GObject *object;
+
+        object = g_object_new (GVC_TYPE_MIXER_SOURCE,
+                               "pa-context", context,
+                               "index", index,
+                               "channel-map", channel_map,
+                               NULL);
+
+        return GVC_MIXER_STREAM (object);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-source.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SOURCE_H
+#define __GVC_MIXER_SOURCE_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SOURCE         (gvc_mixer_source_get_type ())
+#define GVC_MIXER_SOURCE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSource))
+#define GVC_MIXER_SOURCE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass))
+#define GVC_IS_MIXER_SOURCE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE))
+#define GVC_IS_MIXER_SOURCE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE))
+#define GVC_MIXER_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass))
+
+typedef struct GvcMixerSourcePrivate GvcMixerSourcePrivate;
+
+typedef struct
+{
+        GvcMixerStream       parent;
+        GvcMixerSourcePrivate *priv;
+} GvcMixerSource;
+
+typedef struct
+{
+        GvcMixerStreamClass parent_class;
+} GvcMixerSourceClass;
+
+GType               gvc_mixer_source_get_type            (void);
+
+GvcMixerStream *    gvc_mixer_source_new               (pa_context    *context,
+                                                        guint          index,
+                                                        GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SOURCE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-stream-private.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_STREAM_PRIVATE_H
+#define __GVC_MIXER_STREAM_PRIVATE_H
+
+#include <glib-object.h>
+
+#include "gvc-channel-map.h"
+
+G_BEGIN_DECLS
+
+pa_context *        gvc_mixer_stream_get_pa_context  (GvcMixerStream *stream);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_STREAM_PRIVATE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-stream.c
@@ -0,0 +1,1055 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-stream.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+#include "gvc-enum-types.h"
+
+static guint32 stream_serial = 1;
+
+struct GvcMixerStreamPrivate
+{
+        pa_context    *pa_context;
+        guint          id;
+        guint          index;
+        guint          card_index;
+        GvcChannelMap *channel_map;
+        char          *name;
+        char          *description;
+        char          *application_id;
+        char          *icon_name;
+        char          *form_factor;
+        char          *sysfs_path;
+        gboolean       is_muted;
+        gboolean       can_decibel;
+        gboolean       is_event_stream;
+        gboolean       is_virtual;
+        pa_volume_t    base_volume;
+        pa_operation  *change_volume_op;
+        char          *port;
+        char          *human_port;
+        GList         *ports;
+        GvcMixerStreamState state;
+};
+
+enum
+{
+        PROP_0,
+        PROP_ID,
+        PROP_PA_CONTEXT,
+        PROP_CHANNEL_MAP,
+        PROP_INDEX,
+        PROP_NAME,
+        PROP_DESCRIPTION,
+        PROP_APPLICATION_ID,
+        PROP_ICON_NAME,
+        PROP_FORM_FACTOR,
+        PROP_SYSFS_PATH,
+        PROP_VOLUME,
+        PROP_DECIBEL,
+        PROP_IS_MUTED,
+        PROP_CAN_DECIBEL,
+        PROP_IS_EVENT_STREAM,
+        PROP_IS_VIRTUAL,
+        PROP_CARD_INDEX,
+        PROP_PORT,
+        PROP_STATE,
+        N_PROPS
+};
+static GParamSpec *obj_props[N_PROPS] = { NULL, };
+
+static void     gvc_mixer_stream_finalize   (GObject            *object);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GvcMixerStream, gvc_mixer_stream, G_TYPE_OBJECT)
+
+static void
+free_port (GvcMixerStreamPort *p)
+{
+        g_free (p->port);
+        g_free (p->human_port);
+        g_slice_free (GvcMixerStreamPort, p);
+}
+
+static GvcMixerStreamPort *
+dup_port (GvcMixerStreamPort *p)
+{
+        GvcMixerStreamPort *m;
+
+        m = g_slice_new (GvcMixerStreamPort);
+
+        *m = *p;
+        m->port = g_strdup (p->port);
+        m->human_port = g_strdup (p->human_port);
+
+        return m;
+}
+
+G_DEFINE_BOXED_TYPE (GvcMixerStreamPort, gvc_mixer_stream_port, dup_port, free_port)
+
+static guint32
+get_next_stream_serial (void)
+{
+        guint32 serial;
+
+        serial = stream_serial++;
+
+        if ((gint32)stream_serial < 0) {
+                stream_serial = 1;
+        }
+
+        return serial;
+}
+
+pa_context *
+gvc_mixer_stream_get_pa_context (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+        return stream->priv->pa_context;
+}
+
+guint
+gvc_mixer_stream_get_index (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+        return stream->priv->index;
+}
+
+guint
+gvc_mixer_stream_get_id (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+        return stream->priv->id;
+}
+
+const GvcChannelMap *
+gvc_mixer_stream_get_channel_map (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->channel_map;
+}
+
+/**
+ * gvc_mixer_stream_get_volume:
+ * @stream:
+ *
+ * Returns: (type guint32):
+ */
+pa_volume_t
+gvc_mixer_stream_get_volume (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+
+        return (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME];
+}
+
+gdouble
+gvc_mixer_stream_get_decibel (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+
+        return pa_sw_volume_to_dB(
+                        (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]);
+}
+
+/**
+ * gvc_mixer_stream_set_volume:
+ * @stream:
+ * @volume: (type guint32):
+ *
+ * Returns:
+ */
+gboolean
+gvc_mixer_stream_set_volume (GvcMixerStream *stream,
+                              pa_volume_t     volume)
+{
+        pa_cvolume cv;
+
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map);
+        pa_cvolume_scale(&cv, volume);
+
+        if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) {
+                gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE);
+                g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_VOLUME]);
+                return TRUE;
+        }
+
+        return FALSE;
+}
+
+gboolean
+gvc_mixer_stream_set_decibel (GvcMixerStream *stream,
+                              gdouble         db)
+{
+        pa_cvolume cv;
+
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map);
+        pa_cvolume_scale(&cv, pa_sw_volume_from_dB(db));
+
+        if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) {
+                gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE);
+                g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_VOLUME]);
+        }
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_get_is_muted  (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+        return stream->priv->is_muted;
+}
+
+gboolean
+gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+        return stream->priv->can_decibel;
+}
+
+gboolean
+gvc_mixer_stream_set_is_muted  (GvcMixerStream *stream,
+                                gboolean        is_muted)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        if (is_muted != stream->priv->is_muted) {
+                stream->priv->is_muted = is_muted;
+                g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_IS_MUTED]);
+        }
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_can_decibel  (GvcMixerStream *stream,
+                                   gboolean        can_decibel)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        if (can_decibel != stream->priv->can_decibel) {
+                stream->priv->can_decibel = can_decibel;
+                g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_CAN_DECIBEL]);
+        }
+
+        return TRUE;
+}
+
+const char *
+gvc_mixer_stream_get_name (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->name;
+}
+
+const char *
+gvc_mixer_stream_get_description (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->description;
+}
+
+gboolean
+gvc_mixer_stream_set_name (GvcMixerStream *stream,
+                           const char     *name)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_free (stream->priv->name);
+        stream->priv->name = g_strdup (name);
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_NAME]);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_description (GvcMixerStream *stream,
+                                  const char     *description)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_free (stream->priv->description);
+        stream->priv->description = g_strdup (description);
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_DESCRIPTION]);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_is_event_stream (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        return stream->priv->is_event_stream;
+}
+
+gboolean
+gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream,
+                                      gboolean is_event_stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        stream->priv->is_event_stream = is_event_stream;
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_IS_EVENT_STREAM]);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_is_virtual (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        return stream->priv->is_virtual;
+}
+
+gboolean
+gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream,
+                                 gboolean is_virtual)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        stream->priv->is_virtual = is_virtual;
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_IS_VIRTUAL]);
+
+        return TRUE;
+}
+
+const char *
+gvc_mixer_stream_get_application_id (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->application_id;
+}
+
+gboolean
+gvc_mixer_stream_set_application_id (GvcMixerStream *stream,
+                                     const char *application_id)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_free (stream->priv->application_id);
+        stream->priv->application_id = g_strdup (application_id);
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_APPLICATION_ID]);
+
+        return TRUE;
+}
+
+static void
+on_channel_map_volume_changed (GvcChannelMap  *channel_map,
+                               gboolean        set,
+                               GvcMixerStream *stream)
+{
+        if (set == TRUE)
+                gvc_mixer_stream_push_volume (stream);
+
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_VOLUME]);
+}
+
+static gboolean
+gvc_mixer_stream_set_channel_map (GvcMixerStream *stream,
+                                  GvcChannelMap  *channel_map)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        if (channel_map != NULL) {
+                g_object_ref (channel_map);
+        }
+
+        if (stream->priv->channel_map != NULL) {
+                g_signal_handlers_disconnect_by_func (stream->priv->channel_map,
+                                                      on_channel_map_volume_changed,
+                                                      stream);
+                g_object_unref (stream->priv->channel_map);
+        }
+
+        stream->priv->channel_map = channel_map;
+
+        if (stream->priv->channel_map != NULL) {
+                g_signal_connect (stream->priv->channel_map,
+                                  "volume-changed",
+                                  G_CALLBACK (on_channel_map_volume_changed),
+                                  stream);
+
+                g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_CHANNEL_MAP]);
+        }
+
+        return TRUE;
+}
+
+const char *
+gvc_mixer_stream_get_icon_name (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->icon_name;
+}
+
+const char *
+gvc_mixer_stream_get_form_factor (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->form_factor;
+}
+
+const char *
+gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->sysfs_path;
+}
+
+/**
+ * gvc_mixer_stream_get_gicon:
+ * @stream: a #GvcMixerStream
+ *
+ * Returns: (transfer full): a new #GIcon
+ */
+GIcon *
+gvc_mixer_stream_get_gicon (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        if (stream->priv->icon_name == NULL)
+                return NULL;
+        return g_themed_icon_new_with_default_fallbacks (stream->priv->icon_name);
+}
+
+gboolean
+gvc_mixer_stream_set_icon_name (GvcMixerStream *stream,
+                                const char     *icon_name)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_free (stream->priv->icon_name);
+        stream->priv->icon_name = g_strdup (icon_name);
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_ICON_NAME]);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_form_factor (GvcMixerStream *stream,
+                                  const char     *form_factor)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_free (stream->priv->form_factor);
+        stream->priv->form_factor = g_strdup (form_factor);
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_FORM_FACTOR]);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream,
+                                 const char     *sysfs_path)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        g_free (stream->priv->sysfs_path);
+        stream->priv->sysfs_path = g_strdup (sysfs_path);
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_SYSFS_PATH]);
+
+        return TRUE;
+}
+
+/**
+ * gvc_mixer_stream_get_base_volume:
+ * @stream:
+ *
+ * Returns: (type guint32):
+ */
+pa_volume_t
+gvc_mixer_stream_get_base_volume (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+
+        return stream->priv->base_volume;
+}
+
+/**
+ * gvc_mixer_stream_set_base_volume:
+ * @stream:
+ * @base_volume: (type guint32):
+ *
+ * Returns:
+ */
+gboolean
+gvc_mixer_stream_set_base_volume (GvcMixerStream *stream,
+                                  pa_volume_t base_volume)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        stream->priv->base_volume = base_volume;
+
+        return TRUE;
+}
+
+const GvcMixerStreamPort *
+gvc_mixer_stream_get_port (GvcMixerStream *stream)
+{
+        GList *l;
+
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        g_return_val_if_fail (stream->priv->ports != NULL, NULL);
+
+        for (l = stream->priv->ports; l != NULL; l = l->next) {
+                GvcMixerStreamPort *p = l->data;
+                if (g_strcmp0 (stream->priv->port, p->port) == 0) {
+                        return p;
+                }
+        }
+
+        g_assert_not_reached ();
+
+        return NULL;
+}
+
+gboolean
+gvc_mixer_stream_set_port (GvcMixerStream *stream,
+                           const char     *port)
+{
+        GList *l;
+
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+        g_return_val_if_fail (stream->priv->ports != NULL, FALSE);
+
+        g_free (stream->priv->port);
+        stream->priv->port = g_strdup (port);
+
+        g_free (stream->priv->human_port);
+        stream->priv->human_port = NULL;
+
+        for (l = stream->priv->ports; l != NULL; l = l->next) {
+                GvcMixerStreamPort *p = l->data;
+                if (g_str_equal (stream->priv->port, p->port)) {
+                        stream->priv->human_port = g_strdup (p->human_port);
+                        break;
+                }
+        }
+
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_PORT]);
+
+        return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_change_port (GvcMixerStream *stream,
+                              const char     *port)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+        return GVC_MIXER_STREAM_GET_CLASS (stream)->change_port (stream, port);
+}
+
+/**
+ * gvc_mixer_stream_get_ports:
+ *
+ * Return value: (transfer none) (element-type GvcMixerStreamPort):
+ */
+const GList *
+gvc_mixer_stream_get_ports (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+        return stream->priv->ports;
+}
+
+gboolean
+gvc_mixer_stream_set_state (GvcMixerStream      *stream,
+                            GvcMixerStreamState  state)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        if (stream->priv->state != state) {
+                stream->priv->state = state;
+                g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_STATE]);
+        }
+
+        return TRUE;
+}
+
+GvcMixerStreamState
+gvc_mixer_stream_get_state (GvcMixerStream      *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), GVC_STREAM_STATE_INVALID);
+        return stream->priv->state;
+}
+
+static int
+sort_ports (GvcMixerStreamPort *a,
+            GvcMixerStreamPort *b)
+{
+        if (a->priority == b->priority)
+                return 0;
+        if (a->priority > b->priority)
+                return 1;
+        return -1;
+}
+
+/**
+ * gvc_mixer_stream_set_ports:
+ * @ports: (transfer full) (element-type GvcMixerStreamPort):
+ */
+gboolean
+gvc_mixer_stream_set_ports (GvcMixerStream *stream,
+                            GList          *ports)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+        g_return_val_if_fail (stream->priv->ports == NULL, FALSE);
+
+        stream->priv->ports = g_list_sort (ports, (GCompareFunc) sort_ports);
+
+        return TRUE;
+}
+
+guint
+gvc_mixer_stream_get_card_index (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), PA_INVALID_INDEX);
+        return stream->priv->card_index;
+}
+
+gboolean
+gvc_mixer_stream_set_card_index (GvcMixerStream *stream,
+                                 guint           card_index)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        stream->priv->card_index = card_index;
+        g_object_notify_by_pspec (G_OBJECT (stream), obj_props[PROP_CARD_INDEX]);
+
+        return TRUE;
+}
+
+static void
+gvc_mixer_stream_set_property (GObject       *object,
+                               guint          prop_id,
+                               const GValue  *value,
+                               GParamSpec    *pspec)
+{
+        GvcMixerStream *self = GVC_MIXER_STREAM (object);
+
+        switch (prop_id) {
+        case PROP_PA_CONTEXT:
+                self->priv->pa_context = g_value_get_pointer (value);
+                break;
+        case PROP_INDEX:
+                self->priv->index = g_value_get_ulong (value);
+                break;
+        case PROP_ID:
+                self->priv->id = g_value_get_ulong (value);
+                break;
+        case PROP_CHANNEL_MAP:
+                gvc_mixer_stream_set_channel_map (self, g_value_get_object (value));
+                break;
+        case PROP_NAME:
+                gvc_mixer_stream_set_name (self, g_value_get_string (value));
+                break;
+        case PROP_DESCRIPTION:
+                gvc_mixer_stream_set_description (self, g_value_get_string (value));
+                break;
+        case PROP_APPLICATION_ID:
+                gvc_mixer_stream_set_application_id (self, g_value_get_string (value));
+                break;
+        case PROP_ICON_NAME:
+                gvc_mixer_stream_set_icon_name (self, g_value_get_string (value));
+                break;
+        case PROP_FORM_FACTOR:
+                gvc_mixer_stream_set_form_factor (self, g_value_get_string (value));
+                break;
+	case PROP_SYSFS_PATH:
+		gvc_mixer_stream_set_sysfs_path (self, g_value_get_string (value));
+		break;
+        case PROP_VOLUME:
+                gvc_mixer_stream_set_volume (self, g_value_get_ulong (value));
+                break;
+        case PROP_DECIBEL:
+                gvc_mixer_stream_set_decibel (self, g_value_get_double (value));
+                break;
+        case PROP_IS_MUTED:
+                gvc_mixer_stream_set_is_muted (self, g_value_get_boolean (value));
+                break;
+        case PROP_IS_EVENT_STREAM:
+                gvc_mixer_stream_set_is_event_stream (self, g_value_get_boolean (value));
+                break;
+        case PROP_IS_VIRTUAL:
+                gvc_mixer_stream_set_is_virtual (self, g_value_get_boolean (value));
+                break;
+        case PROP_CAN_DECIBEL:
+                gvc_mixer_stream_set_can_decibel (self, g_value_get_boolean (value));
+                break;
+        case PROP_PORT:
+                gvc_mixer_stream_set_port (self, g_value_get_string (value));
+                break;
+        case PROP_STATE:
+                gvc_mixer_stream_set_state (self, g_value_get_enum (value));
+                break;
+        case PROP_CARD_INDEX:
+                self->priv->card_index = g_value_get_long (value);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gvc_mixer_stream_get_property (GObject     *object,
+                               guint        prop_id,
+                               GValue      *value,
+                               GParamSpec  *pspec)
+{
+        GvcMixerStream *self = GVC_MIXER_STREAM (object);
+
+        switch (prop_id) {
+        case PROP_PA_CONTEXT:
+                g_value_set_pointer (value, self->priv->pa_context);
+                break;
+        case PROP_INDEX:
+                g_value_set_ulong (value, self->priv->index);
+                break;
+        case PROP_ID:
+                g_value_set_ulong (value, self->priv->id);
+                break;
+        case PROP_CHANNEL_MAP:
+                g_value_set_object (value, self->priv->channel_map);
+                break;
+        case PROP_NAME:
+                g_value_set_string (value, self->priv->name);
+                break;
+        case PROP_DESCRIPTION:
+                g_value_set_string (value, self->priv->description);
+                break;
+        case PROP_APPLICATION_ID:
+                g_value_set_string (value, self->priv->application_id);
+                break;
+        case PROP_ICON_NAME:
+                g_value_set_string (value, self->priv->icon_name);
+                break;
+        case PROP_FORM_FACTOR:
+                g_value_set_string (value, self->priv->form_factor);
+                break;
+	case PROP_SYSFS_PATH:
+		g_value_set_string (value, self->priv->sysfs_path);
+		break;
+        case PROP_VOLUME:
+                g_value_set_ulong (value,
+                                   pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map)));
+                break;
+        case PROP_DECIBEL:
+                g_value_set_double (value,
+                                    pa_sw_volume_to_dB(pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map))));
+                break;
+        case PROP_IS_MUTED:
+                g_value_set_boolean (value, self->priv->is_muted);
+                break;
+        case PROP_IS_EVENT_STREAM:
+                g_value_set_boolean (value, self->priv->is_event_stream);
+                break;
+        case PROP_IS_VIRTUAL:
+                g_value_set_boolean (value, self->priv->is_virtual);
+                break;
+        case PROP_CAN_DECIBEL:
+                g_value_set_boolean (value, self->priv->can_decibel);
+                break;
+        case PROP_PORT:
+                g_value_set_string (value, self->priv->port);
+                break;
+        case PROP_STATE:
+                g_value_set_enum (value, self->priv->state);
+                break;
+        case PROP_CARD_INDEX:
+                g_value_set_long (value, self->priv->card_index);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static GObject *
+gvc_mixer_stream_constructor (GType                  type,
+                              guint                  n_construct_properties,
+                              GObjectConstructParam *construct_params)
+{
+        GObject       *object;
+        GvcMixerStream *self;
+
+        object = G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+        self = GVC_MIXER_STREAM (object);
+
+        self->priv->id = get_next_stream_serial ();
+
+        return object;
+}
+
+static gboolean
+gvc_mixer_stream_real_change_port (GvcMixerStream *stream,
+                                   const char     *port)
+{
+        return FALSE;
+}
+
+static gboolean
+gvc_mixer_stream_real_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+        return FALSE;
+}
+
+static gboolean
+gvc_mixer_stream_real_change_is_muted (GvcMixerStream *stream,
+                                       gboolean        is_muted)
+{
+        return FALSE;
+}
+
+gboolean
+gvc_mixer_stream_push_volume (GvcMixerStream *stream)
+{
+        pa_operation *op;
+        gboolean ret;
+
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        if (stream->priv->is_event_stream != FALSE)
+                return TRUE;
+
+        g_debug ("Pushing new volume to stream '%s' (%s)",
+                 stream->priv->description, stream->priv->name);
+
+        ret = GVC_MIXER_STREAM_GET_CLASS (stream)->push_volume (stream, (gpointer *) &op);
+        if (ret) {
+                if (stream->priv->change_volume_op != NULL)
+                        pa_operation_unref (stream->priv->change_volume_op);
+                stream->priv->change_volume_op = op;
+        }
+        return ret;
+}
+
+gboolean
+gvc_mixer_stream_change_is_muted (GvcMixerStream *stream,
+                                  gboolean        is_muted)
+{
+        gboolean ret;
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+        ret = GVC_MIXER_STREAM_GET_CLASS (stream)->change_is_muted (stream, is_muted);
+        return ret;
+}
+
+gboolean
+gvc_mixer_stream_is_running (GvcMixerStream *stream)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+        if (stream->priv->change_volume_op == NULL)
+                return FALSE;
+
+        if ((pa_operation_get_state(stream->priv->change_volume_op) == PA_OPERATION_RUNNING))
+                return TRUE;
+
+        pa_operation_unref(stream->priv->change_volume_op);
+        stream->priv->change_volume_op = NULL;
+
+        return FALSE;
+}
+
+static void
+gvc_mixer_stream_class_init (GvcMixerStreamClass *klass)
+{
+        GObjectClass   *gobject_class = G_OBJECT_CLASS (klass);
+
+        gobject_class->constructor = gvc_mixer_stream_constructor;
+        gobject_class->finalize = gvc_mixer_stream_finalize;
+        gobject_class->set_property = gvc_mixer_stream_set_property;
+        gobject_class->get_property = gvc_mixer_stream_get_property;
+
+        klass->push_volume = gvc_mixer_stream_real_push_volume;
+        klass->change_port = gvc_mixer_stream_real_change_port;
+        klass->change_is_muted = gvc_mixer_stream_real_change_is_muted;
+
+        obj_props[PROP_INDEX] = g_param_spec_ulong ("index",
+                                                    "Index",
+                                                    "The index for this stream",
+                                                    0, G_MAXULONG, 0,
+                                                    G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_ID] = g_param_spec_ulong ("id",
+                                                 "id",
+                                                 "The id for this stream",
+                                                 0, G_MAXULONG, 0,
+                                                 G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_CHANNEL_MAP] = g_param_spec_object ("channel-map",
+                                                           "channel map",
+                                                           "The channel map for this stream",
+                                                           GVC_TYPE_CHANNEL_MAP,
+                                                           G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_PA_CONTEXT] = g_param_spec_pointer ("pa-context",
+                                                           "PulseAudio context",
+                                                           "The PulseAudio context for this stream",
+                                                           G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_VOLUME] = g_param_spec_ulong ("volume",
+                                                     "Volume",
+                                                     "The volume for this stream",
+                                                     0, G_MAXULONG, 0,
+                                                     G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_DECIBEL] = g_param_spec_double ("decibel",
+                                                       "Decibel",
+                                                       "The decibel level for this stream",
+                                                       -G_MAXDOUBLE, G_MAXDOUBLE, 0,
+                                                       G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_NAME] = g_param_spec_string ("name",
+                                                    "Name",
+                                                    "Name to display for this stream",
+                                                    NULL,
+                                                    G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_DESCRIPTION] = g_param_spec_string ("description",
+                                                           "Description",
+                                                           "Description to display for this stream",
+                                                           NULL,
+                                                           G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_APPLICATION_ID] = g_param_spec_string ("application-id",
+                                                              "Application identifier",
+                                                              "Application identifier for this stream",
+                                                              NULL,
+                                                              G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_ICON_NAME] = g_param_spec_string ("icon-name",
+                                                         "Icon Name",
+                                                         "Name of icon to display for this stream",
+                                                         NULL,
+                                                         G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_FORM_FACTOR] = g_param_spec_string ("form-factor",
+                                                           "Form Factor",
+                                                           "Device form factor for this stream, as reported by PulseAudio",
+                                                           NULL,
+                                                           G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_SYSFS_PATH] = g_param_spec_string ("sysfs-path",
+                                                          "Sysfs path",
+                                                          "Sysfs path for the device associated with this stream",
+                                                          NULL,
+                                                          G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_IS_MUTED] = g_param_spec_boolean ("is-muted",
+                                                         "is muted",
+                                                         "Whether stream is muted",
+                                                         FALSE,
+                                                         G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_CAN_DECIBEL] = g_param_spec_boolean ("can-decibel",
+                                                            "can decibel",
+                                                            "Whether stream volume can be converted to decibel units",
+                                                            FALSE,
+                                                            G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_IS_EVENT_STREAM] = g_param_spec_boolean ("is-event-stream",
+                                                                "is event stream",
+                                                                "Whether stream's role is to play an event",
+                                                                FALSE,
+                                                                G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_IS_VIRTUAL] = g_param_spec_boolean ("is-virtual",
+                                                           "is virtual stream",
+                                                           "Whether the stream is virtual",
+                                                           FALSE,
+                                                           G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_PORT] = g_param_spec_string ("port",
+                                                    "Port",
+                                                    "The name of the current port for this stream",
+                                                    NULL,
+                                                    G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_STATE] = g_param_spec_enum ("state",
+                                                   "State",
+                                                   "The current state of this stream",
+                                                   GVC_TYPE_MIXER_STREAM_STATE,
+                                                   GVC_STREAM_STATE_INVALID,
+                                                   G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+        obj_props[PROP_CARD_INDEX] = g_param_spec_long ("card-index",
+                                                        "Card index",
+                                                        "The index of the card for this stream",
+                                                        PA_INVALID_INDEX, G_MAXLONG, PA_INVALID_INDEX,
+                                                        G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+
+        g_object_class_install_properties (gobject_class, N_PROPS, obj_props);
+}
+
+static void
+gvc_mixer_stream_init (GvcMixerStream *stream)
+{
+        stream->priv = gvc_mixer_stream_get_instance_private (stream);
+}
+
+static void
+gvc_mixer_stream_finalize (GObject *object)
+{
+        GvcMixerStream *mixer_stream;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_IS_MIXER_STREAM (object));
+
+        mixer_stream = GVC_MIXER_STREAM (object);
+
+        g_return_if_fail (mixer_stream->priv != NULL);
+
+        g_object_unref (mixer_stream->priv->channel_map);
+        mixer_stream->priv->channel_map = NULL;
+
+        g_free (mixer_stream->priv->name);
+        mixer_stream->priv->name = NULL;
+
+        g_free (mixer_stream->priv->description);
+        mixer_stream->priv->description = NULL;
+
+        g_free (mixer_stream->priv->application_id);
+        mixer_stream->priv->application_id = NULL;
+
+        g_free (mixer_stream->priv->icon_name);
+        mixer_stream->priv->icon_name = NULL;
+
+        g_free (mixer_stream->priv->form_factor);
+        mixer_stream->priv->form_factor = NULL;
+
+        g_free (mixer_stream->priv->sysfs_path);
+        mixer_stream->priv->sysfs_path = NULL;
+
+        g_free (mixer_stream->priv->port);
+        mixer_stream->priv->port = NULL;
+
+        g_free (mixer_stream->priv->human_port);
+        mixer_stream->priv->human_port = NULL;
+
+        g_list_free_full (mixer_stream->priv->ports, (GDestroyNotify) free_port);
+        mixer_stream->priv->ports = NULL;
+
+       if (mixer_stream->priv->change_volume_op) {
+               pa_operation_unref(mixer_stream->priv->change_volume_op);
+               mixer_stream->priv->change_volume_op = NULL;
+       }
+
+        G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->finalize (object);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-stream.h
@@ -0,0 +1,146 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_STREAM_H
+#define __GVC_MIXER_STREAM_H
+
+#include <glib-object.h>
+#include "gvc-pulseaudio-fake.h"
+#include "gvc-channel-map.h"
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_STREAM         (gvc_mixer_stream_get_type ())
+#define GVC_MIXER_STREAM(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStream))
+#define GVC_MIXER_STREAM_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass))
+#define GVC_IS_MIXER_STREAM(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_STREAM))
+#define GVC_IS_MIXER_STREAM_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_STREAM))
+#define GVC_MIXER_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass))
+
+typedef struct GvcMixerStreamPrivate GvcMixerStreamPrivate;
+
+typedef struct
+{
+        GObject                parent;
+        GvcMixerStreamPrivate *priv;
+} GvcMixerStream;
+
+typedef struct
+{
+        GObjectClass           parent_class;
+
+        /* vtable */
+        gboolean (*push_volume)     (GvcMixerStream *stream,
+                                     gpointer *operation);
+        gboolean (*change_is_muted) (GvcMixerStream *stream,
+                                     gboolean        is_muted);
+        gboolean (*change_port)     (GvcMixerStream *stream,
+                                     const char     *port);
+} GvcMixerStreamClass;
+
+typedef struct
+{
+        char *port;
+        char *human_port;
+        guint priority;
+        gboolean available;
+} GvcMixerStreamPort;
+
+typedef enum
+{
+        GVC_STREAM_STATE_INVALID,
+        GVC_STREAM_STATE_RUNNING,
+        GVC_STREAM_STATE_IDLE,
+        GVC_STREAM_STATE_SUSPENDED
+} GvcMixerStreamState;
+
+GType               gvc_mixer_stream_port_get_type   (void) G_GNUC_CONST;
+GType               gvc_mixer_stream_get_type        (void) G_GNUC_CONST;
+
+guint               gvc_mixer_stream_get_index       (GvcMixerStream *stream);
+guint               gvc_mixer_stream_get_id          (GvcMixerStream *stream);
+const GvcChannelMap *gvc_mixer_stream_get_channel_map(GvcMixerStream *stream);
+const GvcMixerStreamPort *gvc_mixer_stream_get_port  (GvcMixerStream *stream);
+const GList *       gvc_mixer_stream_get_ports       (GvcMixerStream *stream);
+gboolean            gvc_mixer_stream_change_port     (GvcMixerStream *stream,
+                                                      const char     *port);
+
+pa_volume_t         gvc_mixer_stream_get_volume      (GvcMixerStream *stream);
+gdouble             gvc_mixer_stream_get_decibel     (GvcMixerStream *stream);
+gboolean            gvc_mixer_stream_push_volume     (GvcMixerStream *stream);
+pa_volume_t         gvc_mixer_stream_get_base_volume (GvcMixerStream *stream);
+
+gboolean            gvc_mixer_stream_get_is_muted    (GvcMixerStream *stream);
+gboolean            gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream);
+gboolean            gvc_mixer_stream_change_is_muted (GvcMixerStream *stream,
+                                                      gboolean        is_muted);
+gboolean            gvc_mixer_stream_is_running      (GvcMixerStream *stream);
+const char *        gvc_mixer_stream_get_name        (GvcMixerStream *stream);
+const char *        gvc_mixer_stream_get_icon_name   (GvcMixerStream *stream);
+const char *        gvc_mixer_stream_get_form_factor (GvcMixerStream *stream);
+const char *        gvc_mixer_stream_get_sysfs_path  (GvcMixerStream *stream);
+GIcon *             gvc_mixer_stream_get_gicon       (GvcMixerStream *stream);
+const char *        gvc_mixer_stream_get_description (GvcMixerStream *stream);
+const char *        gvc_mixer_stream_get_application_id (GvcMixerStream *stream);
+gboolean            gvc_mixer_stream_is_event_stream (GvcMixerStream *stream);
+gboolean            gvc_mixer_stream_is_virtual      (GvcMixerStream *stream);
+guint               gvc_mixer_stream_get_card_index  (GvcMixerStream *stream);
+GvcMixerStreamState gvc_mixer_stream_get_state       (GvcMixerStream *stream);
+
+/* private */
+gboolean            gvc_mixer_stream_set_volume      (GvcMixerStream *stream,
+                                                      pa_volume_t     volume);
+gboolean            gvc_mixer_stream_set_decibel     (GvcMixerStream *stream,
+                                                      gdouble         db);
+gboolean            gvc_mixer_stream_set_is_muted    (GvcMixerStream *stream,
+                                                      gboolean        is_muted);
+gboolean            gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream,
+                                                      gboolean        can_decibel);
+gboolean            gvc_mixer_stream_set_name        (GvcMixerStream *stream,
+                                                      const char     *name);
+gboolean            gvc_mixer_stream_set_description (GvcMixerStream *stream,
+                                                      const char     *description);
+gboolean            gvc_mixer_stream_set_icon_name   (GvcMixerStream *stream,
+                                                      const char     *name);
+gboolean            gvc_mixer_stream_set_form_factor (GvcMixerStream *stream,
+                                                      const char     *form_factor);
+gboolean            gvc_mixer_stream_set_sysfs_path  (GvcMixerStream *stream,
+                                                      const char     *sysfs_path);
+gboolean            gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream,
+                                                          gboolean is_event_stream);
+gboolean            gvc_mixer_stream_set_is_virtual  (GvcMixerStream *stream,
+                                                      gboolean is_event_stream);
+gboolean            gvc_mixer_stream_set_application_id (GvcMixerStream *stream,
+                                                         const char *application_id);
+gboolean            gvc_mixer_stream_set_base_volume (GvcMixerStream *stream,
+                                                      pa_volume_t     base_volume);
+gboolean            gvc_mixer_stream_set_port        (GvcMixerStream *stream,
+                                                      const char     *port);
+gboolean            gvc_mixer_stream_set_ports       (GvcMixerStream *stream,
+                                                      GList          *ports);
+gboolean            gvc_mixer_stream_set_card_index  (GvcMixerStream *stream,
+                                                      guint           card_index);
+gboolean            gvc_mixer_stream_set_state       (GvcMixerStream      *stream,
+                                                      GvcMixerStreamState  state);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_STREAM_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-ui-device.c
@@ -0,0 +1,751 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * gvc-mixer-ui-device.c
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ * Copyright (C) 2012 David Henningsson, Canonical Ltd. <david.henningsson@canonical.com>
+ *
+ * gvc-mixer-ui-device.c is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * gvc-mixer-ui-device.c is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "gvc-mixer-ui-device.h"
+#include "gvc-mixer-card.h"
+
+struct GvcMixerUIDevicePrivate
+{
+        gchar                      *first_line_desc;
+        gchar                      *second_line_desc;
+
+        GvcMixerCard               *card;
+        gchar                      *port_name;
+        char                       *icon_name; 
+        guint                       stream_id;
+        guint                       id;
+        gboolean                    port_available;
+
+        /* These two lists contain pointers to GvcMixerCardProfile objects. Those objects are owned by GvcMixerCard. *
+         * TODO: Do we want to add a weak reference to the GvcMixerCard for this reason? */
+        GList                      *supported_profiles; /* all profiles supported by this port.*/
+        GList                      *profiles; /* profiles to be added to combobox, subset of supported_profiles. */
+        GvcMixerUIDeviceDirection   type;
+        gboolean                    disable_profile_swapping;
+        gchar                      *user_preferred_profile;
+};
+
+enum
+{
+        PROP_0,
+        PROP_DESC_LINE_1,
+        PROP_DESC_LINE_2,
+        PROP_CARD,
+        PROP_PORT_NAME,
+        PROP_STREAM_ID,
+        PROP_UI_DEVICE_TYPE,
+        PROP_PORT_AVAILABLE,
+        PROP_ICON_NAME,
+        N_PROPS
+};
+static GParamSpec *obj_props[N_PROPS] = { NULL, };
+
+static void     gvc_mixer_ui_device_finalize   (GObject               *object);
+
+static void     gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device,
+                                                   const char       *icon_name);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerUIDevice, gvc_mixer_ui_device, G_TYPE_OBJECT)
+
+static guint32
+get_next_output_serial (void)
+{
+        static guint32 output_serial = 1;
+        guint32 serial;
+
+        serial = output_serial++;
+
+        if ((gint32)output_serial < 0)
+                output_serial = 1;
+
+        return serial;
+}
+
+static void
+gvc_mixer_ui_device_get_property  (GObject       *object,
+                                   guint         property_id,
+                                   GValue        *value,
+                                   GParamSpec    *pspec)
+{
+        GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object);
+
+        switch (property_id) {
+        case PROP_DESC_LINE_1:
+                g_value_set_string (value, self->priv->first_line_desc);
+                break;
+        case PROP_DESC_LINE_2:
+                g_value_set_string (value, self->priv->second_line_desc);
+                break;
+        case PROP_CARD:
+                g_value_set_pointer (value, self->priv->card);
+                break;
+        case PROP_PORT_NAME:
+                g_value_set_string (value, self->priv->port_name);
+                break;
+        case PROP_STREAM_ID:
+                g_value_set_uint (value, self->priv->stream_id);
+                break;
+        case PROP_UI_DEVICE_TYPE:
+                g_value_set_uint (value, (guint)self->priv->type);
+                break;
+        case PROP_PORT_AVAILABLE:
+                g_value_set_boolean (value, self->priv->port_available);
+                break;
+        case PROP_ICON_NAME:
+                g_value_set_string (value, gvc_mixer_ui_device_get_icon_name (self));
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+                break;
+        }
+}
+
+static void
+gvc_mixer_ui_device_set_property  (GObject      *object,
+                                   guint         property_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+        GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object);
+
+        switch (property_id) {
+        case PROP_DESC_LINE_1:
+                g_free (self->priv->first_line_desc);
+                self->priv->first_line_desc = g_value_dup_string (value);
+                g_debug ("gvc-mixer-output-set-property - 1st line: %s",
+                         self->priv->first_line_desc);
+                break;
+        case PROP_DESC_LINE_2:
+                g_free (self->priv->second_line_desc);
+                self->priv->second_line_desc = g_value_dup_string (value);
+                g_debug ("gvc-mixer-output-set-property - 2nd line: %s",
+                         self->priv->second_line_desc);
+                break;
+        case PROP_CARD:
+                self->priv->card = g_value_get_pointer (value);
+                g_debug ("gvc-mixer-output-set-property - card: %p",
+                         self->priv->card);
+                break;
+        case PROP_PORT_NAME:
+                g_free (self->priv->port_name);
+                self->priv->port_name = g_value_dup_string (value);
+                g_debug ("gvc-mixer-output-set-property - card port name: %s",
+                         self->priv->port_name);
+                break;
+        case PROP_STREAM_ID:
+                self->priv->stream_id = g_value_get_uint (value);
+                g_debug ("gvc-mixer-output-set-property - sink/source id: %i",
+                         self->priv->stream_id);
+                break;
+        case PROP_UI_DEVICE_TYPE:
+                self->priv->type = (GvcMixerUIDeviceDirection) g_value_get_uint (value);
+                g_debug ("gvc-mixer-output-set-property - device type: %s",
+                         self->priv->type == UIDeviceInput ? "input" : "output");
+                break;
+        case PROP_PORT_AVAILABLE:
+                g_debug ("gvc-mixer-output-set-property - old port available %i, value passed in %i",
+                         self->priv->port_available, g_value_get_boolean (value));
+                self->priv->port_available = g_value_get_boolean (value);
+                break;
+        case PROP_ICON_NAME:
+                gvc_mixer_ui_device_set_icon_name (self, g_value_get_string (value));
+                g_debug ("gvc-mixer-output-set-property - icon name: %s",
+                         self->priv->icon_name);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+                break;
+        }
+}
+
+static GObject *
+gvc_mixer_ui_device_constructor (GType                  type,
+                                 guint                  n_construct_properties,
+                                 GObjectConstructParam *construct_params)
+{
+        GObject           *object;
+        GvcMixerUIDevice  *self;
+
+        object = G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+        self = GVC_MIXER_UI_DEVICE (object);
+        self->priv->id = get_next_output_serial ();
+        self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+        return object;
+}
+
+static void
+gvc_mixer_ui_device_init (GvcMixerUIDevice *device)
+{
+        device->priv = gvc_mixer_ui_device_get_instance_private (device);
+}
+
+static void
+gvc_mixer_ui_device_dispose (GObject *object)
+{
+        GvcMixerUIDevice *device;
+
+        g_return_if_fail (object != NULL);
+        g_return_if_fail (GVC_MIXER_UI_DEVICE (object));
+
+        device = GVC_MIXER_UI_DEVICE (object);
+
+        g_clear_pointer (&device->priv->port_name, g_free);
+        g_clear_pointer (&device->priv->icon_name, g_free);
+        g_clear_pointer (&device->priv->first_line_desc, g_free);
+        g_clear_pointer (&device->priv->second_line_desc, g_free);
+        g_clear_pointer (&device->priv->profiles, g_list_free);
+        g_clear_pointer (&device->priv->supported_profiles, g_list_free);
+        g_clear_pointer (&device->priv->user_preferred_profile, g_free);
+
+        G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->dispose (object);
+}
+
+static void
+gvc_mixer_ui_device_finalize (GObject *object)
+{
+        G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->finalize (object);
+}
+
+static void
+gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass)
+{
+        GObjectClass* object_class = G_OBJECT_CLASS (klass);
+
+        object_class->constructor = gvc_mixer_ui_device_constructor;
+        object_class->dispose = gvc_mixer_ui_device_dispose;
+        object_class->finalize = gvc_mixer_ui_device_finalize;
+        object_class->set_property = gvc_mixer_ui_device_set_property;
+        object_class->get_property = gvc_mixer_ui_device_get_property;
+
+        obj_props[PROP_DESC_LINE_1] =
+                g_param_spec_string ("description",
+                                     "Description construct prop",
+                                     "Set first line description",
+                                     "no-name-set",
+                                     G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_DESC_LINE_2] =
+                g_param_spec_string ("origin",
+                                     "origin construct prop",
+                                     "Set second line description name",
+                                     "no-name-set",
+                                     G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_CARD] =
+                g_param_spec_pointer ("card",
+                                      "Card from pulse",
+                                      "Set/Get card",
+                                      G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_PORT_NAME] =
+                g_param_spec_string ("port-name",
+                                     "port-name construct prop",
+                                     "Set port-name",
+                                     NULL,
+                                     G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_STREAM_ID] =
+                g_param_spec_uint ("stream-id",
+                                   "stream id assigned by gvc-stream",
+                                   "Set/Get stream id",
+                                   0,
+                                   G_MAXUINT,
+                                   GVC_MIXER_UI_DEVICE_INVALID,
+                                   G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_UI_DEVICE_TYPE] =
+                g_param_spec_uint ("type",
+                                   "ui-device type",
+                                   "determine whether its an input and output",
+                                   0, 1, 0,
+                                   G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_PORT_AVAILABLE] =
+                g_param_spec_boolean ("port-available",
+                                      "available",
+                                      "determine whether this port is available",
+                                      FALSE,
+                                      G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+        obj_props[PROP_ICON_NAME] =
+                g_param_spec_string ("icon-name",
+                                     "Icon Name",
+                                     "Name of icon to display for this card",
+                                     NULL,
+                                     G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
+
+        g_object_class_install_properties (object_class, N_PROPS, obj_props);
+}
+
+/* Removes the part of the string that starts with skip_prefix
+ * ie. corresponding to the other direction.
+ * Normally either "input:" or "output:"
+ *
+ * Example: if given the input string "output:hdmi-stereo+input:analog-stereo" and
+ * skip_prefix "input:", the resulting string is "output:hdmi-stereo".
+ *
+ * The returned string must be freed with g_free().
+ */
+static gchar *
+get_profile_canonical_name (const gchar *profile_name, const gchar *skip_prefix)
+{
+        gchar *result = NULL;
+        gchar **s;
+        guint i;
+
+        /* optimisation for the simple case. */
+        if (strstr (profile_name, skip_prefix) == NULL)
+                return g_strdup (profile_name);
+
+        s = g_strsplit (profile_name, "+", 0);
+        for (i = 0; i < g_strv_length (s); i++) {
+                if (g_str_has_prefix (s[i], skip_prefix))
+                        continue;
+                if (result == NULL)
+                        result = g_strdup (s[i]);
+                else {
+                        gchar *c = g_strdup_printf("%s+%s", result, s[i]);
+                        g_free(result);
+                        result = c;
+                }
+        }
+
+        g_strfreev(s);
+
+        if (!result)
+                return g_strdup("off");
+
+        return result;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, const gchar *profile)
+{
+        const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:";
+        gchar *target_cname = get_profile_canonical_name (profile, skip_prefix);
+        GList *l;
+        gchar *result = NULL;
+
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+        g_return_val_if_fail (profile != NULL, NULL);
+
+        for (l = device->priv->profiles; l != NULL; l = l->next) {
+                gchar *canonical_name;
+                GvcMixerCardProfile* p = l->data;
+                canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
+                if (strcmp (canonical_name, target_cname) == 0)
+                        result = p->profile;
+                g_free (canonical_name);
+        }
+
+        g_free (target_cname);
+        g_debug ("Matching profile for '%s' is '%s'", profile, result ? result : "(null)");
+        return result;
+}
+
+
+static void
+add_canonical_names_of_profiles (GvcMixerUIDevice *device,
+                                 const GList      *in_profiles,
+                                 GHashTable       *added_profiles,
+                                 const gchar      *skip_prefix,
+                                 gboolean          only_canonical)
+{
+        const GList *l;
+
+        for (l = in_profiles; l != NULL; l = l->next) {
+                gchar *canonical_name;
+                GvcMixerCardProfile* p = l->data;
+
+                canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
+                g_debug ("The canonical name for '%s' is '%s'", p->profile, canonical_name);
+
+                /* Have we already added the canonical version of this profile? */
+                if (g_hash_table_contains (added_profiles, canonical_name)) {
+                        g_free (canonical_name);
+                        continue;
+                }
+
+                if (only_canonical && strcmp (p->profile, canonical_name) != 0) {
+                        g_free (canonical_name);
+                        continue;
+                }
+
+                g_free (canonical_name);
+
+                /* https://bugzilla.gnome.org/show_bug.cgi?id=693654
+                 * Don't add a profile that will make the UI device completely disappear */
+                if (p->n_sinks == 0 && p->n_sources == 0)
+                        continue;
+
+                g_debug ("Adding profile to combobox: '%s' - '%s'", p->profile, p->human_profile);
+                g_hash_table_insert (added_profiles, g_strdup (p->profile), p);
+                device->priv->profiles = g_list_append (device->priv->profiles, p);
+        }
+}
+
+/**
+ * gvc_mixer_ui_device_set_profiles:
+ * @in_profiles: (element-type Gvc.MixerCardProfile): a list of GvcMixerCardProfile
+ *
+ * Assigns value to
+ *  - device->priv->profiles (profiles to be added to combobox)
+ *  - device->priv->supported_profiles (all profiles of this port)
+ *  - device->priv->disable_profile_swapping (whether to show the combobox)
+ *
+ * This method attempts to reduce the list of profiles visible to the user by figuring out
+ * from the context of that device (whether it's an input or an output) what profiles
+ * actually provide an alternative.
+ *
+ * It does this by the following.
+ *  - It ignores off profiles.
+ *  - It takes the canonical name of the profile. That name is what you get when you
+ *    ignore the other direction.
+ *  - In the first iteration, it only adds the names of canonical profiles - i e
+ *    when the other side is turned off.
+ *  - Normally the first iteration covers all cases, but sometimes (e g bluetooth)
+ *    it doesn't, so add other profiles whose canonical name isn't already added
+ *    in a second iteration.
+ */
+void
+gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device,
+                                  const GList      *in_profiles)
+{
+        GHashTable *added_profiles;
+        const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:";
+
+        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
+
+        g_debug ("Set profiles for '%s'", gvc_mixer_ui_device_get_description(device));
+
+        g_clear_pointer (&device->priv->supported_profiles, g_list_free);
+        g_clear_pointer (&device->priv->profiles, g_list_free);
+
+        if (in_profiles == NULL)
+                return;
+
+        device->priv->supported_profiles = g_list_copy ((GList*) in_profiles);
+
+        added_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+        /* Run two iterations: First, add profiles which are canonical themselves,
+         * Second, add profiles for which the canonical name is not added already. */
+
+        add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, TRUE);
+        add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, FALSE);
+
+        /* TODO: Consider adding the "Off" profile here */
+
+        device->priv->disable_profile_swapping = g_hash_table_size (added_profiles) <= 1;
+        g_hash_table_destroy (added_profiles);
+}
+
+/**
+ * gvc_mixer_ui_device_get_best_profile:
+ * @selected: (allow-none): The selected profile or its canonical name or %NULL for any profile
+ * @current: The currently selected profile
+ *
+ * Returns: (transfer none): a profile name, valid as long as the UI device profiles are.
+ */
+const gchar *
+gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device,
+                                      const gchar      *selected,
+                                      const gchar      *current)
+{
+        GList *candidates, *l;
+        const gchar *result;
+        const gchar *skip_prefix;
+        gchar *canonical_name_selected;
+
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+        g_return_val_if_fail (current != NULL, NULL);
+
+        if (device->priv->type == UIDeviceInput)
+                skip_prefix = "output:";
+        else
+                skip_prefix = "input:";
+
+        /* First make a list of profiles acceptable to switch to */
+        canonical_name_selected = NULL;
+        if (selected)
+                canonical_name_selected = get_profile_canonical_name (selected, skip_prefix);
+
+	candidates = NULL;
+        for (l = device->priv->supported_profiles; l != NULL; l = l->next) {
+                gchar *canonical_name;
+                GvcMixerCardProfile* p = l->data;
+                canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
+                if (!canonical_name_selected || strcmp (canonical_name, canonical_name_selected) == 0) {
+                        candidates = g_list_append (candidates, p);
+                        g_debug ("Candidate for profile switching: '%s'", p->profile);
+                }
+                g_free (canonical_name);
+        }
+
+        if (!candidates) {
+                g_warning ("No suitable profile candidates for '%s'", selected ? selected : "(null)");
+                g_free (canonical_name_selected);
+                return current;
+        }
+
+        /* 1) Maybe we can skip profile switching altogether? */
+        result = NULL;
+        for (l = candidates; (result == NULL) && (l != NULL); l = l->next) {
+                GvcMixerCardProfile* p = l->data;
+                if (strcmp (current, p->profile) == 0)
+                        result = p->profile;
+        }
+
+        /* 2) Try to keep the other side unchanged if possible */
+        if (result == NULL) {
+                guint prio = 0;
+                const gchar *skip_prefix_reverse = device->priv->type == UIDeviceInput ? "input:" : "output:";
+                gchar *current_reverse = get_profile_canonical_name (current, skip_prefix_reverse);
+                for (l = candidates; l != NULL; l = l->next) {
+                        gchar *p_reverse;
+                        GvcMixerCardProfile* p = l->data;
+                        p_reverse = get_profile_canonical_name (p->profile, skip_prefix_reverse);
+                        g_debug ("Comparing '%s' (from '%s') with '%s', prio %d", p_reverse, p->profile, current_reverse, p->priority); 
+                        if (strcmp (p_reverse, current_reverse) == 0 && (!result || p->priority > prio)) {
+                                result = p->profile;
+                                prio = p->priority;
+                        }
+                        g_free (p_reverse);
+                }
+                g_free (current_reverse);
+        }
+
+        /* 3) All right, let's just pick the profile with highest priority.
+         * TODO: We could consider asking a GUI question if this stops streams
+         * in the other direction */
+        if (result == NULL) {
+                guint prio = 0;
+                for (l = candidates; l != NULL; l = l->next) {
+                        GvcMixerCardProfile* p = l->data;
+                        if ((p->priority > prio) || !result) {
+                                result = p->profile;
+                                prio = p->priority;
+                        }
+                }
+        }
+
+        g_list_free (candidates);
+        g_free (canonical_name_selected);
+        return result;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device)
+{
+        GvcMixerCardProfile *profile;
+
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        if (device->priv->card == NULL) {
+                g_warning ("Device did not have an appropriate card");
+                return NULL;
+        }
+
+        profile = gvc_mixer_card_get_profile (device->priv->card);
+        return gvc_mixer_ui_device_get_matching_profile (device, profile->profile);
+}
+
+gboolean
+gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+        return device->priv->disable_profile_swapping;
+}
+
+/**
+ * gvc_mixer_ui_device_get_profiles:
+ * @device:
+ *
+ * Returns: (transfer none) (element-type Gvc.MixerCardProfile):
+ */
+GList*
+gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        return device->priv->profiles;
+}
+
+/**
+ * gvc_mixer_ui_device_get_supported_profiles:
+ * @device:
+ *
+ * Returns: (transfer none) (element-type Gvc.MixerCardProfile):
+ */
+GList*
+gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        return device->priv->supported_profiles;
+}
+
+guint
+gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0);
+
+        return device->priv->id;
+}
+
+guint
+gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0);
+
+        return device->priv->stream_id;
+}
+
+void
+gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *self)
+{
+        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (self));
+
+        self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        return device->priv->first_line_desc;
+}
+
+const char *
+gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        if (device->priv->icon_name)
+                return device->priv->icon_name;
+
+        if (device->priv->card)
+                return gvc_mixer_card_get_icon_name (device->priv->card);
+
+        return NULL;
+}
+
+static void
+gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device,
+                                   const char       *icon_name)
+{
+        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
+
+        g_free (device->priv->icon_name);
+        device->priv->icon_name = g_strdup (icon_name);
+        g_object_notify_by_pspec (G_OBJECT (device), obj_props[PROP_ICON_NAME]);
+}
+
+
+/**
+ * gvc_mixer_ui_device_get_gicon:
+ * @device:
+ *
+ * Returns: (transfer full):
+ */
+GIcon *
+gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device)
+{
+        const char *icon_name;
+
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        icon_name = gvc_mixer_ui_device_get_icon_name (device);
+
+        if (icon_name != NULL)
+                return g_themed_icon_new_with_default_fallbacks (icon_name);
+        else
+                return NULL;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        return device->priv->second_line_desc;
+}
+
+const gchar*
+gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        return device->priv->user_preferred_profile;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device)
+{
+        GList *last;
+        GvcMixerCardProfile *profile;
+
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        last = g_list_last (device->priv->supported_profiles);
+        profile = last->data;
+
+        return profile->profile;
+}
+
+void
+gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device,
+                                                const gchar      *profile)
+{
+        g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
+        g_return_if_fail (profile != NULL);
+
+        g_free (device->priv->user_preferred_profile);
+        device->priv->user_preferred_profile = g_strdup (profile);
+}
+
+const gchar *
+gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+        return device->priv->port_name;
+}
+
+gboolean
+gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+        return (device->priv->port_name != NULL);
+}
+
+gboolean
+gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device)
+{
+        g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+        return (device->priv->type == UIDeviceOutput);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-mixer-ui-device.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * gvc-mixer-ui-device.h is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GVC_MIXER_UI_DEVICE_H_
+#define _GVC_MIXER_UI_DEVICE_H_
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_UI_DEVICE             (gvc_mixer_ui_device_get_type ())
+#define GVC_MIXER_UI_DEVICE(obj)             (G_TYPE_CHECK_INSTANCE_CAST ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDevice))
+#define GVC_MIXER_UI_DEVICE_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST ((klass), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass))
+#define GVC_IS_MIXER_UI_DEVICE(obj)          (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GVC_TYPE_MIXER_UI_DEVICE))
+#define GVC_IS_MIXER_UI_DEVICE_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE ((klass), GVC_TYPE_MIXER_UI_DEVICE))
+#define GVC_MIXER_UI_DEVICE_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass))
+
+#define GVC_MIXER_UI_DEVICE_INVALID          0
+
+typedef struct GvcMixerUIDevicePrivate GvcMixerUIDevicePrivate;
+
+typedef struct
+{
+        GObjectClass parent_class;
+} GvcMixerUIDeviceClass;
+
+typedef struct
+{
+        GObject parent_instance;
+        GvcMixerUIDevicePrivate *priv;
+} GvcMixerUIDevice;
+
+typedef enum
+{
+        UIDeviceInput,
+        UIDeviceOutput,
+} GvcMixerUIDeviceDirection;
+
+GType gvc_mixer_ui_device_get_type (void) G_GNUC_CONST;
+
+guint          gvc_mixer_ui_device_get_id                      (GvcMixerUIDevice *device);
+guint          gvc_mixer_ui_device_get_stream_id               (GvcMixerUIDevice *device);
+const gchar *  gvc_mixer_ui_device_get_description             (GvcMixerUIDevice *device);
+const gchar *  gvc_mixer_ui_device_get_icon_name               (GvcMixerUIDevice *device);
+GIcon *        gvc_mixer_ui_device_get_gicon                   (GvcMixerUIDevice *device);
+const gchar *  gvc_mixer_ui_device_get_origin                  (GvcMixerUIDevice *device);
+const gchar *  gvc_mixer_ui_device_get_port                    (GvcMixerUIDevice *device);
+const gchar *  gvc_mixer_ui_device_get_best_profile            (GvcMixerUIDevice *device,
+                                                                const gchar      *selected,
+                                                                const gchar      *current);
+const gchar *  gvc_mixer_ui_device_get_active_profile          (GvcMixerUIDevice* device);
+const gchar *  gvc_mixer_ui_device_get_matching_profile        (GvcMixerUIDevice *device,
+                                                                const gchar      *profile);
+const gchar *  gvc_mixer_ui_device_get_user_preferred_profile  (GvcMixerUIDevice *device);
+const gchar *  gvc_mixer_ui_device_get_top_priority_profile    (GvcMixerUIDevice *device);
+GList *        gvc_mixer_ui_device_get_profiles                (GvcMixerUIDevice *device);
+GList *        gvc_mixer_ui_device_get_supported_profiles      (GvcMixerUIDevice *device);
+gboolean       gvc_mixer_ui_device_should_profiles_be_hidden   (GvcMixerUIDevice *device);
+void           gvc_mixer_ui_device_set_profiles                (GvcMixerUIDevice *device,
+                                                                const GList      *in_profiles);
+void           gvc_mixer_ui_device_set_user_preferred_profile  (GvcMixerUIDevice *device,
+                                                                const gchar      *profile);
+void           gvc_mixer_ui_device_invalidate_stream           (GvcMixerUIDevice *device);
+gboolean       gvc_mixer_ui_device_has_ports                   (GvcMixerUIDevice *device);
+gboolean       gvc_mixer_ui_device_is_output                   (GvcMixerUIDevice *device);
+
+G_END_DECLS
+
+#endif /* _GVC_MIXER_UI_DEVICE_H_ */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/gvc-pulseaudio-fake.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_PULSEAUDIO_FAKE_H
+#define __GVC_PULSEAUDIO_FAKE_H
+
+#ifndef PA_API_VERSION
+#define pa_channel_position_t int
+#define pa_volume_t guint32
+#define pa_context gpointer
+#endif /* PA_API_VERSION */
+
+#endif /* __GVC_PULSEAUDIO_FAKE_H */
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/libgnome-volume-control.doap
@@ -0,0 +1,32 @@
+<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+         xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+         xmlns:foaf="http://xmlns.com/foaf/0.1/"
+         xmlns:gnome="http://api.gnome.org/doap-extensions#"
+         xmlns="http://usefulinc.com/ns/doap#">
+
+  <name xml:lang="en">libgnome-volume-control</name>
+  <shortdesc xml:lang="en">GObject layer for PulseAudio</shortdesc>
+  <description>
+    This library contains code to access PulseAudio using a GObject
+    based library, shared between gnome-control-center, gnome-settings-daemon
+    and gnome-shell. It is not API stable, and it is meant to be used
+    as a submodule.
+  </description>
+
+  <!-- <category rdf:resource="http://api.gnome.org/doap-extensions#desktop" /> -->
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Emmanuele Bassi</foaf:name>
+      <foaf:mbox rdf:resource="mailto:ebassi@gnome.org"/>
+      <gnome:userid>ebassi</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+  <author>
+    <foaf:Person>
+      <foaf:name>Bastien Nocera</foaf:name>
+      <foaf:mbox rdf:resource="mailto:hadess@hadess.net" />
+      <gnome:userid>hadess</gnome:userid>
+    </foaf:Person>
+  </author>
+</Project>
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/meson.build
@@ -0,0 +1,137 @@
+project('gvc', 'c',
+  meson_version: '>= 0.42.0',
+  default_options: ['static=true']
+)
+
+assert(meson.is_subproject(), 'This project is only intended to be used as a subproject!')
+
+gnome = import('gnome')
+
+pkglibdir = get_option('pkglibdir')
+pkgdatadir = get_option('pkgdatadir')
+
+cdata = configuration_data()
+cdata.set_quoted('GETTEXT_PACKAGE', get_option('package_name'))
+cdata.set_quoted('PACKAGE_VERSION', get_option('package_version'))
+
+libgvc_gir_headers = [
+  'gvc-channel-map.h',
+  'gvc-mixer-card.h',
+  'gvc-mixer-control.h',
+  'gvc-mixer-event-role.h',
+  'gvc-mixer-sink.h',
+  'gvc-mixer-sink-input.h',
+  'gvc-mixer-source.h',
+  'gvc-mixer-source-output.h',
+  'gvc-mixer-stream.h',
+  'gvc-mixer-ui-device.h'
+]
+
+libgvc_enums = gnome.mkenums_simple('gvc-enum-types',
+  sources: libgvc_gir_headers
+)
+
+libgvc_gir_sources = [
+  'gvc-channel-map.c',
+  'gvc-mixer-card.c',
+  'gvc-mixer-control.c',
+  'gvc-mixer-event-role.c',
+  'gvc-mixer-sink.c',
+  'gvc-mixer-sink-input.c',
+  'gvc-mixer-source.c',
+  'gvc-mixer-source-output.c',
+  'gvc-mixer-stream.c',
+  'gvc-mixer-ui-device.c'
+]
+
+libgvc_no_gir_sources = [
+  'gvc-mixer-card-private.h',
+  'gvc-mixer-stream-private.h',
+  'gvc-channel-map-private.h',
+  'gvc-mixer-control-private.h',
+  'gvc-pulseaudio-fake.h'
+]
+
+libgvc_deps = [
+  dependency('gio-2.0'),
+  dependency('gobject-2.0'),
+  dependency('libpulse', version: '>= 12.99.3'),
+  dependency('libpulse-mainloop-glib')
+]
+
+enable_alsa = get_option('alsa')
+if enable_alsa
+  libgvc_deps += dependency('alsa')
+endif
+cdata.set('HAVE_ALSA', enable_alsa)
+
+enable_static = get_option('static')
+enable_introspection = get_option('introspection')
+
+assert(not enable_static or not enable_introspection, 'Currently meson requires a shared library for building girs.')
+assert(enable_static or pkglibdir != '', 'Installing shared library, but pkglibdir is unset!')
+
+c_args = ['-DG_LOG_DOMAIN="Gvc"']
+
+if enable_introspection
+  c_args += '-DWITH_INTROSPECTION'
+endif
+
+if enable_static
+  libgvc_static = static_library('gvc',
+    sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums,
+    dependencies: libgvc_deps,
+    c_args: c_args
+  )
+
+  libgvc = libgvc_static
+else
+  if pkglibdir == ''
+    error('Installing shared library, but pkglibdir is unset!')
+  endif
+
+  libgvc_shared = shared_library('gvc',
+    sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums,
+    dependencies: libgvc_deps,
+    c_args: c_args,
+    install_dir: pkglibdir,
+    install: true
+  )
+
+  libgvc = libgvc_shared
+endif
+
+if enable_introspection
+  assert(pkgdatadir != '', 'Installing introspection, but pkgdatadir is unset!')
+
+  libgvc_gir = gnome.generate_gir(libgvc,
+    sources: libgvc_gir_sources + libgvc_gir_headers + libgvc_enums,
+    nsversion: '1.0',
+    namespace: 'Gvc',
+    includes: ['Gio-2.0', 'GObject-2.0'],
+    extra_args: ['-DWITH_INTROSPECTION', '--quiet'],
+    install_dir_gir: pkgdatadir,
+    install_dir_typelib: pkglibdir,
+    install: true
+  )
+endif
+
+if enable_alsa
+  executable('test-audio-device-selection',
+    sources: 'test-audio-device-selection.c',
+    link_with: libgvc,
+    dependencies: libgvc_deps,
+    c_args: c_args
+  )
+endif
+
+libgvc_dep = declare_dependency(
+  link_with: libgvc,
+  include_directories: include_directories('.'),
+  dependencies: libgvc_deps
+)
+
+configure_file(
+  output: 'config.h',
+  configuration: cdata
+)
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/meson_options.txt
@@ -0,0 +1,41 @@
+option('package_name',
+  type: 'string',
+  value: '',
+  description: 'The value for the GETTEXT_PACKAGE define.'
+)
+
+option('package_version',
+  type: 'string',
+  value: '',
+  description: 'The value for the PACKAGE_VERSION define.'
+)
+
+option('pkglibdir',
+  type: 'string',
+  value: '',
+  description: 'The private directory the shared library/typelib will be installed into.'
+)
+
+option('pkgdatadir',
+  type: 'string',
+  value: '',
+  description: 'The private directory the gir file will be installed into.'
+)
+
+option('alsa',
+  type: 'boolean',
+  value: true,
+  description: 'Build ALSA support.'
+)
+
+option('static',
+  type: 'boolean',
+  value: false,
+  description: 'Build as a static library.'
+)
+
+option('introspection',
+  type: 'boolean',
+  value: false,
+  description: 'Build gobject-introspection support'
+)
--- /dev/null
+++ gnome-shell-48.0/subprojects/gvc/test-audio-device-selection.c
@@ -0,0 +1,84 @@
+
+#include <stdio.h>
+#include <locale.h>
+#include <pulse/pulseaudio.h>
+#include "gvc-mixer-control.h"
+
+#define MAX_ATTEMPTS 3
+
+typedef struct {
+	GvcHeadsetPortChoice choice;
+	const char *name;
+} AudioSelectionChoice;
+
+static AudioSelectionChoice audio_selection_choices[] = {
+	{ GVC_HEADSET_PORT_CHOICE_HEADPHONES,   "headphones" },
+	{ GVC_HEADSET_PORT_CHOICE_HEADSET,      "headset" },
+	{ GVC_HEADSET_PORT_CHOICE_MIC,          "microphone" },
+};
+
+static void
+audio_selection_needed (GvcMixerControl      *volume,
+			guint                 id,
+			gboolean              show_dialog,
+			GvcHeadsetPortChoice  choices,
+			gpointer              user_data)
+{
+	const char *args[G_N_ELEMENTS (audio_selection_choices) + 1];
+	guint i, n;
+	int response = -1;
+
+	if (!show_dialog) {
+		g_print ("--- Audio selection not needed anymore for id %d\n", id);
+		return;
+	}
+
+	n = 0;
+	for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) {
+		if (choices & audio_selection_choices[i].choice)
+			args[n++] = audio_selection_choices[i].name;
+	}
+	args[n] = NULL;
+
+	g_print ("+++ Audio selection needed for id %d\n", id);
+	g_print ("    Choices are:\n");
+	for (i = 0; args[i] != NULL; i++)
+		g_print ("    %d. %s\n", i + 1, args[i]);
+
+	for (i = 0; response < 0 && i < MAX_ATTEMPTS; i++) {
+		int res;
+
+		g_print ("What is your choice?\n");
+		if (scanf ("%d", &res) == 1 &&
+		    res > 0 &&
+		    res < (int) g_strv_length ((char **)  args)) {
+			response = res;
+			break;
+		}
+	}
+
+	gvc_mixer_control_set_headset_port (volume,
+					    id,
+					    audio_selection_choices[response - 1].choice);
+}
+
+int main (int argc, char **argv)
+{
+	GMainLoop *loop;
+	GvcMixerControl *volume;
+
+	setlocale (LC_ALL, "");
+
+	loop = g_main_loop_new (NULL, FALSE);
+
+	volume = gvc_mixer_control_new ("GNOME Volume Control test");
+	g_signal_connect (volume,
+			  "audio-device-selection-needed",
+			  G_CALLBACK (audio_selection_needed),
+			  NULL);
+	gvc_mixer_control_open (volume);
+
+	g_main_loop_run (loop);
+
+	return 0;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/.eslintignore
@@ -0,0 +1,6 @@
+# Copied from Jasmine, use the original project's coding style
+lib/jasmine.js
+test/jasmineIntegrationTest.js
+
+# Deliberate syntax error
+test/fixtures/syntaxErrorSpec.js
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/.eslintrc.yml
@@ -0,0 +1,235 @@
+---
+env:
+  es6: true
+extends: 'eslint:recommended'
+rules:
+  array-bracket-newline:
+    - error
+    - consistent
+  array-bracket-spacing:
+    - error
+    - never
+  array-callback-return: error
+  arrow-parens:
+    - error
+    - as-needed
+  arrow-spacing: error
+  block-scoped-var: error
+  block-spacing: error
+  brace-style: error
+  camelcase:
+    - error
+    - properties: never
+      allow: [^vfunc_, ^on_, _instance_init]
+  comma-dangle:
+    - error
+    - arrays: always-multiline
+      objects: always-multiline
+      functions: never
+  comma-spacing:
+    - error
+    - before: false
+      after: true
+  comma-style:
+    - error
+    - last
+  computed-property-spacing: error
+  curly:
+    - error
+    - multi-or-nest
+    - consistent
+  dot-location:
+    - error
+    - property
+  eol-last: error
+  eqeqeq: error
+  func-call-spacing: error
+  func-name-matching: error
+  func-style:
+    - error
+    - declaration
+    - allowArrowFunctions: true
+  indent:
+    - error
+    - 4
+    - ignoredNodes:
+      # Allow not indenting the body of GObject.registerClass, since in the
+      # future it's intended to be a decorator
+      - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child'
+      # Allow dedenting chained member expressions
+      MemberExpression: 'off'
+  key-spacing:
+    - error
+    - beforeColon: false
+      afterColon: true
+  keyword-spacing:
+    - error
+    - before: true
+      after: true
+  linebreak-style:
+    - error
+    - unix
+  lines-between-class-members: error
+  max-nested-callbacks: error
+  max-statements-per-line: error
+  new-parens: error
+  no-array-constructor: error
+  no-await-in-loop: error
+  no-caller: error
+  no-constant-condition:
+    - error
+    - checkLoops: false
+  no-div-regex: error
+  no-empty:
+    - error
+    - allowEmptyCatch: true
+  no-extra-bind: error
+  no-extra-parens:
+    - error
+    - all
+    - conditionalAssign: false
+      nestedBinaryExpressions: false
+      returnAssign: false
+  no-implicit-coercion:
+    - error
+    - allow:
+      - '!!'
+  no-invalid-this: error
+  no-iterator: error
+  no-label-var: error
+  no-lonely-if: error
+  no-loop-func: error
+  no-nested-ternary: error
+  no-new-object: error
+  no-new-wrappers: error
+  no-octal-escape: error
+  no-proto: error
+  no-prototype-builtins: 'off'
+  no-restricted-properties:
+    - error
+    - object: imports
+      property: format
+      message: Use template strings
+    - object: Lang
+      property: copyProperties
+      message: Use Object.assign()
+    - object: Lang
+      property: bind
+      message: Use arrow notation or Function.prototype.bind()
+    - object: Lang
+      property: Class
+      message: Use ES6 classes
+  no-restricted-syntax:
+    - error
+    - selector: >-
+        MethodDefinition[key.name="_init"] >
+        FunctionExpression[params.length=1] >
+        BlockStatement[body.length=1]
+        CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] >
+        Identifier:first-child
+      message: _init() that only calls super._init() is unnecessary
+    - selector: >-
+        MethodDefinition[key.name="_init"] >
+        FunctionExpression[params.length=0] >
+        BlockStatement[body.length=1]
+        CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"]
+      message: _init() that only calls super._init() is unnecessary
+    - selector: BinaryExpression[operator="instanceof"][right.name="Array"]
+      message: Use Array.isArray()
+  no-return-assign: error
+  no-return-await: error
+  no-self-compare: error
+  no-shadow: error
+  no-shadow-restricted-names: error
+  no-spaced-func: error
+  no-tabs: error
+  no-template-curly-in-string: error
+  no-throw-literal: error
+  no-trailing-spaces: error
+  no-undef-init: error
+  no-unneeded-ternary: error
+  no-unused-expressions: error
+  no-unused-vars:
+    - error
+    # Vars use a suffix _ instead of a prefix because of file-scope private vars
+    - varsIgnorePattern: (^unused|_$)
+      argsIgnorePattern: ^(unused|_)
+  no-useless-call: error
+  no-useless-computed-key: error
+  no-useless-concat: error
+  no-useless-constructor: error
+  no-useless-rename: error
+  no-useless-return: error
+  no-whitespace-before-property: error
+  no-with: error
+  nonblock-statement-body-position:
+    - error
+    - below
+  object-curly-newline:
+    - error
+    - consistent: true
+  object-curly-spacing: error
+  object-shorthand: error
+  operator-assignment: error
+  operator-linebreak: error
+  prefer-const: error
+  prefer-destructuring: error
+  prefer-numeric-literals: error
+  prefer-promise-reject-errors: error
+  prefer-rest-params: error
+  prefer-spread: error
+  prefer-template: error
+  quotes:
+    - error
+    - single
+    - avoidEscape: true
+  require-await: error
+  rest-spread-spacing: error
+  semi:
+    - error
+    - always
+  semi-spacing:
+    - error
+    - before: false
+      after: true
+  semi-style: error
+  space-before-blocks: error
+  space-before-function-paren:
+    - error
+    - named: never
+      # for `function ()` and `async () =>`, preserve space around keywords
+      anonymous: always
+      asyncArrow: always
+  space-in-parens: error
+  space-infix-ops:
+    - error
+    - int32Hint: false
+  space-unary-ops: error
+  spaced-comment: error
+  switch-colon-spacing: error
+  symbol-description: error
+  template-curly-spacing: error
+  template-tag-spacing: error
+  unicode-bom: error
+  valid-jsdoc:
+    - error
+    - requireReturn: false
+  wrap-iife:
+    - error
+    - inside
+  yield-star-spacing: error
+  yoda: error
+globals:
+  ARGV: readonly
+  Debugger: readonly
+  GIRepositoryGType: readonly
+  globalThis: readonly
+  imports: readonly
+  Intl: readonly
+  log: readonly
+  logError: readonly
+  print: readonly
+  printerr: readonly
+parserOptions:
+  ecmaVersion: 2020
+  sourceType: module
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/.jscsrc
@@ -0,0 +1,41 @@
+{
+    "disallowEmptyBlocks": true,
+    "disallowMixedSpacesAndTabs": true,
+    "disallowSpaceAfterObjectKeys": true,
+    "disallowSpaceAfterPrefixUnaryOperators": ["!", "~", "++", "--", "+", "-"],
+    "disallowSpaceBeforeBinaryOperators": [","],
+    "disallowSpacesInNamedFunctionExpression": {
+        "beforeOpeningRoundBrace": true
+    },
+    "disallowSpacesInsideParentheses": true,
+    "disallowTrailingWhitespace": true,
+    "disallowPaddingNewlinesInBlocks": true,
+    "requireCapitalizedConstructors": true,
+    "requireCommaBeforeLineBreak": true,
+    "requireLineFeedAtFileEnd": true,
+    "requireParenthesesAroundIIFE": true,
+    "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
+    "requireSpaceAfterKeywords": [
+        "if",
+        "else",
+        "for",
+        "while",
+        "do",
+        "switch",
+        "return",
+        "try",
+        "catch",
+        "function"
+    ],
+    "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
+    "requireSpaceBeforeBlockStatements": true,
+    "requireSpacesInAnonymousFunctionExpression": {
+        "beforeOpeningRoundBrace": true,
+        "beforeOpeningCurlyBrace": true
+    },
+    "requireSpacesInFunctionDeclaration": {
+        "beforeOpeningCurlyBrace": true
+    },
+    "validateIndentation": 4,
+    "validateLineBreaks": "LF"
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/.jshintrc
@@ -0,0 +1,33 @@
+{
+    "esnext": true,
+    "evil": false,
+    "indent": 4,
+    "moz": true,
+    "multistr": true,
+    "sub": true,
+    "trailing": true,
+    "undef": true,
+    "unused": true,
+    "predef": [
+        "addCustomEqualityTester",
+        "afterAll",
+        "afterEach",
+        "ARGV",
+        "beforeAll",
+        "beforeEach",
+        "describe",
+        "expect",
+        "fdescribe",
+        "fit",
+        "imports",
+        "it",
+        "jasmine",
+        "pending",
+        "print",
+        "printerr",
+        "spyOn",
+        "window",
+        "xdescribe",
+        "xit"
+    ]
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/.meson-subproject-wrap-hash.txt
@@ -0,0 +1 @@
+d4d34b8ce38a662f9c8fe3560e3fea8e91653bc6d6aaeafa951361a99a9cdfa2
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/.travis.yml
@@ -0,0 +1,25 @@
+---
+dist: focal
+addons:
+  apt:
+    packages:
+      - gjs
+      - meson
+      - npm
+matrix:
+  include:
+    - name: Linter
+      script: |
+        npm install -g eslint &&
+        eslint bin/* src test
+    - name: Unit tests
+      script: |
+        meson _build &&
+        ninja -C _build &&
+        RUN_THOROUGH_TESTS=yes meson test -C _build
+    - name: Installed test
+      script: |
+        meson _build &&
+        ninja -C _build &&
+        sudo ninja -C _build install &&
+        jasmine
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Philip Chimento
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/NEWS.md
@@ -0,0 +1,110 @@
+# Release Notes
+
+## 3.10.1 - July 14, 2024
+
+- The internal copy of the Jasmine library has been updaetd to version 3.10.1, which brings:
+  - Support for async before/it/after functions (2.7.0)
+  - Add `nothing()` matcher (2.8.0)
+  - Add `jasmine.arrayWithExactContents()` tester (2.8.0)
+  - Support for `jasmine.any(Symbol)` (2.9.0)
+  - `jasmine.any(Object)` no longer matches null (3.0)
+  - Add `jasmine.truthy()`, `jasmine.falsy()`, `jasmine.empty()`, and `jasmine.notEmpty()` testers (3.1.0)
+  - Add `spyOnAllFunctions()` (3.2.0)
+  - Add `expectAsync()`, and `toBeResolved()` and `toBeRejected()` matchers (3.2.0)
+  - Add `withContext()` for extra debugging information (3.3.0)
+  - Add `toBeRejectedWith()` matcher (3.3.0)
+  - Support for custom async matchers with `jasmine.addAsyncMatchers()` (3.5.0)
+  - Add `jasmine.setDefaultSpyStrategy()` (3.5.0)
+  - Add `jasmine.mapContaining()` and `jasmine.setContaining()` tester (3.5.0)
+  - Add `toBeTrue()` and `toBeFalse()` matchers (3.5.0)
+  - Add `toHaveBeenCalledOnceWith()` matcher (3.6.0)
+  - Add `toHaveSize()` matcher (3.6.0)
+  - Add `toBePending()` matcher (3.6.0)
+  - Add `already` property of async specs (3.8.0)
+  - Add `spy.calls.thisFor()` (3.8.0)
+  - Add `jasmine.stringContaining()` tester (3.10.0)
+  - All other bug fixes and improvements contained in the intervening versions; see Jasmine's release notes
+- It is now possible to use Jasmine GJS as a Meson subproject.
+- Jasmine GJS now depends on GJS 1.68.0.
+- Thanks to Florian Müllner and Martín Abente Lahaye for contributing.
+
+## 2.6.4 - September 13, 2020
+
+- The internal copy of the Jasmine library has been updated to version 2.6.4, which brings:
+  - Add `toBeNegativeInfinity()`, `toBePositiveInfinity()`, `toHaveBeenCalledBefore()` matchers
+  - Add `spyOnProperty()` for get/set accessors
+  - Add support for ES6 sets to `toContain()` and `toEqual()`
+  - Bug fixes included in 2.6.0, 2.6.1, 2.6.2, 2.6.3 and 2.6.4
+- Thanks to Andy Holmes for contributing.
+
+## 2.5.2 - September 10, 2020
+
+- The internal copy of the Jasmine library has been updated to version 2.5.2, which brings:
+  - Add `toBeGreaterThanOrEqual()` and `toBeLessThanOrEqual()` matchers
+  - Bug fixes included in 2.5.0, 2.5.1 and 2.5.2
+- Thanks to Andy Holmes for contributing.
+
+## 2.4.1 - Sepetember 4, 2020
+
+- The internal copy of the Jasmine library has been updated to version 2.4.1, which brings:
+  - Run jasmine's specs in random order
+  - Add support for returning run details for reporting randomness
+  - Bug fixes included in 2.4.0
+- Thanks to Andy Holmes for contributing.
+
+## 2.3.4 - August 30, 2020
+
+- Fixed a regression in 2.3.0 which caused include paths in the config file to be treated as additional spec paths.
+- The TAP reporter now outputs the test plan at the beginning instead of the end, as it should.
+- The internal copy of the Jasmine library has been updated to version 2.3.4, which brings in the minor bugfixes from 2.3.1, 2.3.2, and 2.3.3 as well.
+- Thanks to Andy Holmes for contributing.
+
+## 2.3.0 - August 25, 2020
+
+- Added a `--debug` command line flag which will run the tests under GDB or the debugger of your choice.
+- Added an `--interpreter` command line flag which allows using a different interpreter than whichever copy of `gjs` is in your path.
+- Fixed several bugs around the loading of spec files.
+- Improved error messages.
+- Jasmine GJS now depends on GJS 1.58.0.
+- The internal copy of the Jasmine library has been updated to 2.3.0, which brings:
+  - `done.fail()` for asynchronous specs.
+  - `toContain()` can be used for finding substrings.
+  - `toThrow()` can be used to check that a particular value was thrown.
+- Thanks to Niv Sardi, Bart Libert and Andy Holmes for contributing.
+
+## 2.2.1 - June 30, 2015
+
+- We now use `/usr/bin/env` to locate jasmine-gjs which allows Jasmine to be used with a development version of GJS. (Thanks to Sam Spilsbury)
+- We don't exit with `System.exit()` on success, because that bypasses the GJS interpreter shutdown actions. (Thanks to Sam Spilsbury)
+- The internal copy of the Jasmine library has been updated to version 2.2.1, which is a minor bugfix release.
+
+## 2.2.0 - May 17, 2015
+
+- You can now specify file patterns to exclude using the `--exclude`
+  command line option.
+  Previously you could only do this via the config file.
+- You can now set environment variables in the config file using the
+  `"environment"` key.
+- When you specify a directory for JUnit reports to be placed in, that
+  directory will now be created if it doesn't already exist.
+- If the argument to `--junit` is not an absolute path, then the path
+  will be resolved relative to the current directory.
+  However, you can now resolve it relative to a different path by
+  setting the environment variable `JASMINE_JUNIT_REPORTS_DIR`.
+- The verbose and TAP reporters now report the reason why a spec has
+  been marked pending, if any reason has been given.
+- The internal copy of the Jasmine library has been updated to version
+  2.2.0, including the following features:
+  - the `toThrowError()` matcher to expect a particular exception
+  - the `jasmine.anything()` object to match anything
+  - the `jasmine.arrayContaining()` object to match an array containing
+    all of the given objects
+  - the `jasmine.stringMatching()` object to match a string that matches
+    the given regular expression or substring
+  - custom matching objects with `asymmetricMatch` methods
+  - per-spec timeouts with an extra argument to `it()`, `beforeEach()`,
+    and `afterEach()`
+
+## 2.1.3 - March 4, 2015
+
+- Initial release.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/README.md
@@ -0,0 +1,232 @@
+[![Build Status](https://travis-ci.org/ptomato/jasmine-gjs.png?branch=master)](https://travis-ci.org/ptomato/jasmine-gjs)
+
+# Jasmine for GJS
+
+[Jasmine](https://github.com/jasmine/jasmine) is a BDD (behavior-driven
+development) framework for JavaScript.
+
+## Contents
+
+This module allows you to run Jasmine specs for your GJS code.
+The output will be displayed in your terminal.
+
+## Installation
+
+From Git:
+
+```sh
+git clone https://github.com/ptomato/jasmine-gjs
+cd jasmine-gjs
+meson _build
+ninja -C _build
+sudo ninja -C _build install
+```
+
+From a tarball:
+
+```sh
+tar xJf jasmine-gjs-2.1.3.tar.xz
+cd jasmine-gjs-2.1.3
+meson _build
+ninja -C _build
+sudo ninja -C _build install
+```
+
+## Usage
+
+To run your test suite, pass its directory to the `jasmine` command:
+
+```bash
+jasmine mytestsuitedir
+```
+
+To run an individual spec:
+
+```bash
+jasmine path/to/spec.js
+```
+
+## Options
+
+`--no-color`: Don't output color in the terminal.
+(Opposite of `--color`.)
+
+`--exclude <file>`: Don't run specs that match `file` (which may
+contain `?` and `*` wildcards.)
+This is useful if you pass a directory of specs that also contains
+utility modules or specs that you don't want to run by default.
+You may specify this option more than once with different arguments.
+
+`--verbose`: Output verbose results, formatted similarly to Mocha's
+output.
+(The default output is formatted the same as Jasmine's Node.JS runner's
+output.)
+
+`--tap`: Output according to the
+[Test Anything Protocol](http://testanything.org/).
+This is useful when integrating with Automake (see below.)
+
+`--junit [<file>]`: In addition to the console output, print a JUnit-style
+XML report to `file`.
+If no file is given, the default is `report.xml` in the current working
+directory.
+If you don't specify an absolute path but you do set the environment variable `JASMINE_JUNIT_REPORTS_DIR`, then the report will be placed there instead.
+This allows you to use `$$tst` as the argument to `--junit` in your Makefile to mirror your test directory structure in your report directory.
+
+`--config <file>`: Use `file` as Jasmine's configuration file instead of
+the default `jasmine.json`.
+
+`--no-config`: Don't load a configuration file, even if one is present.
+
+## Configuration file
+
+Maybe you don't want to keep typing the same options every time you run
+your tests.
+In that case, you can create a configuration file called `jasmine.json`.
+Jasmine will look for it in the current working directory by default.
+
+If you configure your test suites in the configuration file, then you
+can start Jasmine simply by running
+
+```bash
+jasmine
+```
+
+The file should be a JSON file.
+You can use the following keys:
+
+`include_paths`: (string or array of strings)
+Prepend these paths to GJS's include paths, in order of priority,
+highest first.
+(The same as specifying `-I` to GJS or defining the `GJS_PATH`
+environment variable.)
+Relative paths are resolved relative to the configuration file's
+location.
+
+`options`: (string or array of strings)
+Use these command-line options for Jasmine by default.
+Options given on the command line will override these in the event of a
+conflict.
+
+`exclude`: (string or array of strings)
+Don't run specs in these files or directories.
+They may contain `*` and `?` as wildcards.
+(The same as specifying `--exclude` on the command line.)
+
+`spec_files`: (string or array of strings)
+Run specs in these files or directories.
+The same as specifying files or directories on the command line.
+If you specify any on the command line, then this option is entirely
+ignored; this is useful for running your entire test suite by default
+but retaining the ability to run just one spec file on the command line.
+Relative paths are resolved relative to the configuration file's
+location.
+
+`environment`: (object with string properties)
+Execute specs with a modified environment.
+Each property of the given object is added as an environment variable.
+If a property's value is `null`, then any existing environment variable
+of that name is unset.
+This is mainly useful if your Javascript code uses a private C library;
+you can add `LD_LIBRARY_PATH` and `GI_TYPELIB_PATH` to `environment` in
+order to expose that library to GJS's GObject introspection repository.
+
+## Integration with Meson
+
+If using Meson, you might want to integrate your test suite into your build process.
+Meson can parse the output of the TAP reporter automatically and display a full report at the end.
+
+Put this code in `meson.build`:
+
+```
+jasmine = find_program('jasmine')
+
+test(test_name, jasmine, args: [test_file, '--tap', '--no-config'],
+    protocol: 'tap')
+```
+
+Where `test_name` and `test_file` are the name of your test and the JS
+script to run, respectively.
+
+## Integration with Autotools
+
+If using autotools, you might want to integrate your test suite into
+your makefiles.
+The TAP reporter produces output that Automake can parse and display
+while the tests are running.
+That way, you have more immediate feedback and can see problems directly
+from your build output.
+
+Put this code in `configure.ac`:
+
+```
+AC_PROG_AWK
+AC_REQUIRE_AUX_FILE([tap-driver.sh])
+```
+
+Then re-run `autogen.sh` or `autoreconf` to install the TAP driver.
+
+Then, put this code in your `Makefile.am`:
+
+```make
+JS_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
+JS_LOG_DRIVER_FLAGS = --comments
+
+TESTS = path/to/spec1.js path/to/spec2.js
+TEST_EXTENSIONS = .js
+JS_LOG_COMPILER = jasmine
+AM_JS_LOG_FLAGS = --tap --no-config
+```
+
+Don't forget to `EXTRA_DIST` your spec files too.
+Now Jasmine will run your spec files one by one as part of `make check`.
+
+With this configuration, the TAP driver displays all the extra
+diagnostic information about suites starting and finishing, and
+expectation messages in case of failure.
+If you don't want that, remove the `JS_LOG_DRIVER_FLAGS` line.
+
+### A note on extra files
+
+If you use on-disk test fixtures, you should note that someone may be
+building your software with separate source and build trees.
+Notably, Meson does this.
+In that case, you have to make sure that your fixtures can be found both
+when running tests from the source directory and from a separate build
+directory.
+I suggest defining an environment variable in your makefile that tells
+where the fixtures can be found.
+For example, in Jasmine GJS's own tests, this line is in `meson.build`:
+
+```
+test_env = {
+    'SRCDIR': meson.current_source_dir(),
+}
+```
+
+The test code then reads the environment variable like this:
+
+```js
+let envSrcdir = GLib.getenv('SRCDIR');
+const SRCDIR = envSrcdir? envSrcdir + '/' : '';
+
+doSomethingWithFixture(SRCDIR + 'path/to/fixture');
+```
+
+## Support
+
+Please file issues here at
+[GitHub](https://github.com/ptomato/jasmine-gjs/issues).
+
+## Credits
+
+The idea of Jasmine for GJS was first explored by my coworkers Devin
+Ekins and Sam Spilsbury:
+[eos-jasmine](https://github.com/endlessm/eos-jasmine).
+This code, however, was unashamedly cribbed from Pivotal Labs'
+[jasmine-npm](https://github.com/jasmine/jasmine-npm), then expanded
+into what it is now.
+
+Copyright (c) 2015 Philip Chimento.
+This software is licensed under the MIT License &mdash; because Jasmine
+and Jasmine NPM are too.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/bin/jasmine-runner.in
@@ -0,0 +1,34 @@
+#!/usr/bin/env -S gjs -m
+
+/* global jasmineImporter */
+
+import GLib from 'gi://GLib';
+import System from 'system';
+
+const pkgdatadir = GLib.getenv('TEST_PKGDATADIR') ?? '@pkgdatadir@';
+const jasmineMod = GLib.getenv('TEST_JASMINE_MOD') ?? '@jasmine_mod@';
+
+// Create a separate GJS importer object for Jasmine modules, so that Jasmine's
+// modules are not exposed to test code (e.g. client code might have its own
+// Utils module.)
+// This means that all imports within Jasmine must use jasmineImporter rather
+// than imports. That includes imports of Jasmine modules in the tests. It would
+// be better to test a separate copy of Jasmine code, but importing most modules
+// registers a GType, and we cannot register two GTypes with the same name in
+// the same process.
+
+const oldSearchPath = imports.searchPath.slice();  // make a copy
+imports.searchPath.unshift(GLib.path_get_dirname(pkgdatadir));
+globalThis.jasmineImporter = imports[jasmineMod];
+imports.searchPath = oldSearchPath;
+
+const base = `file://${pkgdatadir}`;
+
+const Command = await import(`${base}/command.js`);
+const JasmineBoot = await import(`${base}/jasmineBoot.js`);
+
+// Do not conflict with global "jasmine" object
+const _jasmine = new JasmineBoot.Jasmine();
+_jasmine.installAPI(globalThis);
+
+System.exit(await Command.run(_jasmine, ARGV, 10));
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/bin/jasmine.in
@@ -0,0 +1,35 @@
+#!/usr/bin/env -S gjs -m
+
+import GLib from 'gi://GLib';
+import System from 'system';
+
+const pkglibexecdir = GLib.getenv('TEST_PKGLIBEXECDIR') ?? '@pkglibexecdir@';
+const pkgdatadir = GLib.getenv('TEST_PKGDATADIR') ?? '@pkgdatadir@';
+
+const runnerPath = `${pkglibexecdir}/jasmine-runner`;
+const base = `file://${pkgdatadir}`;
+
+const Config = await import(`${base}/config.js`);
+const Options = await import(`${base}/options.js`);
+
+const [files, options] = Options.parseOptions(ARGV);
+
+if (options.version) {
+    print('Jasmine @PACKAGE_VERSION@');
+    System.exit(0);
+}
+
+const config = Config.loadConfig(options);
+
+// Launch Jasmine in a subprocess so we can control the environment
+const launcher = Config.prepareLauncher(config, options);
+let args = Config.configToArgs(config, files, options);
+args.unshift(runnerPath);  // argv[0]
+args = Config.wrapArgs(args, config, options);
+const process = launcher.spawnv(args);
+process.wait(null);
+
+if (process.get_if_exited())
+    System.exit(process.get_exit_status());
+
+System.exit(1);
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/debian/changelog
@@ -0,0 +1,58 @@
+jasmine-gjs (3.10.1-1) UNRELEASED; urgency=low
+
+  * Upgrade to 3.10.1
+
+ -- Philip Chimento <philip.chimento@gmail.com>  Sun, 14 Jul 2024 13:59:43 -0700
+
+jasmine-gjs (2.6.4-1) unstable; urgency=low
+
+  * Upgrade to 2.6.4
+
+ -- Andy Holmes <andrew.g.r.holmes@gmail.com>  Thu, 10 Sep 2020 21:47:34 -0700
+
+jasmine-gjs (2.5.2-1) unstable; urgency=low
+
+  * Upgrade to 2.5.2
+
+ -- Andy Holmes <andrew.g.r.holmes@gmail.com>  Fri, 04 Sep 2020 19:20:51 -0700
+
+jasmine-gjs (2.4.1-1) unstable; urgency=low
+
+  * Upgrade to 2.4.1
+
+ -- Andy Holmes <andrew.g.r.holmes@gmail.com>  Sat, 29 Aug 2020 19:46:28 -0700
+
+jasmine-gjs (2.3.4-1) unstable; urgency=low
+
+  * Upgrade to 2.3.4
+
+ -- Andy Holmes <andrew.g.r.holmes@gmail.com>  Wed, 26 Aug 2020 16:34:43 -0700
+
+jasmine-gjs (2.3.0-1) unstable; urgency=low
+
+  * Upgrade to 2.3.0
+  * control: Bump GJS requirement to 1.58.0
+  * control, rules: Meson build system
+  * control: Update to modern versions of standards and debhelper-compat
+
+ -- Philip Chimento <philip.chimento@gmail.com>  Tue, 25 Aug 2020 22:19:41 -0700
+
+jasmine-gjs (2.2.1-1) unstable; urgency=low
+
+  * Upgrade to 2.2.1
+
+ -- Philip Chimento <philip.chimento@gmail.com>  Tue, 30 Jun 2015 14:24:39 -0700
+
+jasmine-gjs (2.2.0-1) unstable; urgency=low
+
+  * Upgrade to 2.2.0
+  * copyright: Update copyright year of upstream Jasmine
+  * docs: Add new NEWS.md file
+
+ -- Philip Chimento <philip.chimento@gmail.com>  Mon, 18 May 2015 01:03:37 -0700
+
+jasmine-gjs (2.1.3-1) unstable; urgency=low
+
+  * Initial release
+
+ -- Philip Chimento <philip.chimento@gmail.com>  Sun, 01 Mar 2015 15:25:17 -0800
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/debian/control
@@ -0,0 +1,19 @@
+Source: jasmine-gjs
+Section: devel
+Priority: optional
+Maintainer: Philip Chimento <philip.chimento@gmail.com>
+Build-Depends: debhelper-compat (= 12),
+               gir1.2-glib-2.0,
+               gjs (>= 1.71.1),
+               meson (>= 0.58.0)
+Standards-Version: 4.5.0
+Homepage: https://github.com/ptomato/jasmine-gjs
+
+Package: jasmine-gjs
+Architecture: all
+Depends: ${misc:Depends},
+         gir1.2-glib-2.0,
+         gjs (>= 1.71.1)
+Description: Behavior-driven development framework for GJS
+ This module allows you to run Jasmine specs for your GJS code. The output will
+ be displayed in your terminal.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/debian/copyright
@@ -0,0 +1,31 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: jasmine-gjs
+Source: https://github.com/ptomato/jasmine-gjs
+License: Expat
+
+Files: *
+Copyright: 2015 Philip Chimento <philip.chimento@gmail.com>
+License: Expat
+
+Files: lib/jasmine.js
+Copyright: 2008-2015 Pivotal Labs
+License: Expat
+
+License: Expat
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/debian/docs
@@ -0,0 +1,2 @@
+README.md
+NEWS.md
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/debian/rules
@@ -0,0 +1,12 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+%:
+	dh $@
+
+# Print out test log on failure, so that we can diagnose failures on OBS
+override_dh_auto_test:
+	dh_auto_test || (find . -name testlog.txt | xargs cat ; exit 1)
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/jasmine-gjs.spec
@@ -0,0 +1,66 @@
+Name: jasmine-gjs
+Version: 3.10.1
+Release: 1%{?dist}
+Summary: A behavior-driven development framework for GJS
+
+License: MIT
+URL: https://github.com/ptomato/jasmine-gjs
+Source0: https://github.com/ptomato/jasmine-gjs/releases/download/3.10.1/jasmine-gjs-3.10.1.tar.xz
+
+BuildArch: noarch
+BuildRequires: gjs >= 1.71.1
+BuildRequires: gobject-introspection
+BuildRequires: meson >= 0.58.0
+Requires: gjs >= 1.71.1
+Requires: gobject-introspection
+
+%description
+This module allows you to run Jasmine specs for your GJS code. The output will
+be displayed in your terminal.
+
+%prep
+%autosetup
+
+
+%build
+%meson
+%meson_build
+
+
+%install
+%meson_install
+
+
+%check
+%meson_test
+
+
+%files
+%doc README.md
+%doc NEWS.md
+%doc COPYING
+%doc %{_mandir}/man1/jasmine.1.gz
+%{_bindir}/jasmine
+%{_libexecdir}/%{name}/
+%{_datadir}/%{name}/
+
+
+%changelog
+* Sun Jul 14 2024 Philip Chimento <philip.chimento@gmail.com> - 3.10.1-1
+- Update requirements for version 3.10.1.
+* Thu Sep 10 2020 Andy Holmes <andrew.g.r.holmes@gmail.com> - 2.6.4-1
+- Update to version 2.6.4.
+* Fri Sep 4 2020 Andy Holmes <andrew.g.r.holmes@gmail.com> - 2.5.2-1
+- Update to version 2.5.2.
+* Sat Aug 29 2020 Andy Holmes <andrew.g.r.holmes@gmail.com> - 2.4.1-1
+- Update to version 2.4.1.
+* Wed Aug 26 2020 Andy Holmes <andrew.g.r.holmes@gmail.com> - 2.3.4-1
+- Update to version 2.3.4.
+* Mon Aug 24 2020 Philip Chimento <philip.chimento@gmail.com> - 2.3.0-1
+- Update to version 2.3.0.
+* Tue Jun 30 2015 Philip Chimento <philip.chimento@gmail.com> - 2.2.1-1
+- Update to version 2.2.1.
+* Sun May 17 2015 Philip Chimento <philip.chimento@gmail.com> - 2.2.0-1
+- Update to version 2.2.0.
+* Wed Mar 4 2015 Philip Chimento <philip.chimento@gmail.com> - 2.1.3-1
+- Initial packaging.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/jasmine.json
@@ -0,0 +1,5 @@
+{
+    "exclude": ["test/fixtures/*", "test/focusedSpecIntegrationTest.js"],
+    "spec_files": "test",
+    "options": "--verbose"
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/jasmine.man
@@ -0,0 +1,107 @@
+.\" Copyright 2015 Philip Chimento <philip.chimento@gmail.com>
+.TH JASMINE 1 "March 1, 2015"
+.\" Please adjust this date whenever revising the manpage.
+.SH NAME
+jasmine \- run Jasmine test suites
+.SH SYNOPSIS
+.B jasmine
+.RB [ \-\-help ]
+.RB [ \-\-color ]
+.RB [ \-\-no\-color ]
+.RB [ \-\-config
+.IR file ]
+.RB [ \-\-no\-config ]
+.RB [ \-\-exclude
+.IR file ]
+.RB [ \-\-verbose ]
+.RB [ \-\-tap ]
+.RB [ \-\-junit
+.RI [ file "]] " files " ..."
+.SH DESCRIPTION
+Jasmine is a behavior-driven development framework for the GJS Javascript
+interpreter.
+The
+.B jasmine
+command is a test runner for Jasmine test suites that has several different
+output formats.
+.PP
+The
+.I files
+specified on the command line are imported and all Jasmine specs defined therein
+are executed.
+You may also specify whole directories in
+.IR files ,
+in which case the directories are searched recursively and files ending in
+.I .js
+will be imported.
+.SH OPTIONS
+.TP
+.B \-\-color
+Print output in color, if the chosen output format supports it.
+.TP
+.BI \-\-config " file"
+Use
+.I file
+as the configuration file, instead of
+.IR jasmine.json .
+.TP
+.BI \-\-exclude " file"
+Don't execute specs in
+.I file
+(which may also be a wildcard pattern) even if they are in a directory specified
+on the command line.
+(Don't forget to escape wildcards so the shell doesn't expand them.)
+.TP
+.B \-\-help
+Show summary of options.
+.TP
+.BI \-\-junit " file"
+In addition to the format used to report results to the console (default,
+.BR \-\-verbose ,
+or
+.BR \-\-tap ,)
+output test results to a JUnit-style XML file to
+.IR file ,
+suitable for integration with Jenkins.
+If
+.I file
+is not given, output to
+.I report.xml
+in the current directory.
+.TP
+.B \-\-no\-color
+Do not print output in color, even if the chosen output format supports it.
+.TP
+.B \-\-no\-config
+Don't use a configuration file, even if one is present.
+.TP
+.B \-\-tap
+Report results with the TAP (Test Anything Protocol) reporter, suitable for
+integration with Automake.
+.TP
+.B \-\-verbose
+Report results with the verbose reporter, similar to Mocha's default reporter.
+.SH EXIT STATUS
+The
+.B jasmine
+utility exits with 0 on success, or with 1 if any tests failed or an internal
+error occurred.
+.SH ENVIRONMENT
+The JASMINE_JUNIT_REPORTS_DIR environment variable controls where JUnit-style
+XML reports are placed, if a relative path is passed to
+.B \-\-junit
+on the command line.
+.PP
+The JASMINE_UNINSTALLED environment variable can be used for debugging Jasmine.
+It is not recommended to set this variable during normal use.
+.SH FILES
+By default, Jasmine reads from a configuration file called
+.I jasmine.json
+in the current directory, if such a file exists.
+This can be influenced with the
+.B \-\-config
+and
+.B \-\-no\-config
+options.
+.SH BUGS
+Report bugs at https://github.com/ptomato/jasmine-gjs/issues on GitHub.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/lib/jasmine.js
@@ -0,0 +1,10766 @@
+/*
+Copyright (c) 2008-2022 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+// eslint-disable-next-line no-unused-vars
+var getJasmineRequireObj = (function(jasmineGlobal) {
+  var jasmineRequire;
+
+  if (
+    typeof module !== 'undefined' &&
+    module.exports &&
+    typeof exports !== 'undefined'
+  ) {
+    if (typeof global !== 'undefined') {
+      jasmineGlobal = global;
+    } else {
+      jasmineGlobal = {};
+    }
+    jasmineRequire = exports;
+  } else {
+    if (
+      typeof window !== 'undefined' &&
+      typeof window.toString === 'function' &&
+      window.toString() === '[object GjsGlobal]'
+    ) {
+      jasmineGlobal = window;
+    }
+    jasmineRequire = jasmineGlobal.jasmineRequire = {};
+  }
+
+  function getJasmineRequire() {
+    return jasmineRequire;
+  }
+
+  getJasmineRequire().core = function(jRequire) {
+    var j$ = {};
+
+    jRequire.base(j$, jasmineGlobal);
+    j$.util = jRequire.util(j$);
+    j$.errors = jRequire.errors();
+    j$.formatErrorMsg = jRequire.formatErrorMsg();
+    j$.Any = jRequire.Any(j$);
+    j$.Anything = jRequire.Anything(j$);
+    j$.CallTracker = jRequire.CallTracker(j$);
+    j$.MockDate = jRequire.MockDate(j$);
+    j$.getClearStack = jRequire.clearStack(j$);
+    j$.Clock = jRequire.Clock();
+    j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$);
+    j$.Deprecator = jRequire.Deprecator(j$);
+    j$.Env = jRequire.Env(j$);
+    j$.deprecatingThisProxy = jRequire.deprecatingThisProxy(j$);
+    j$.deprecatingSuiteProxy = jRequire.deprecatingSuiteProxy(j$);
+    j$.deprecatingSpecProxy = jRequire.deprecatingSpecProxy(j$);
+    j$.StackTrace = jRequire.StackTrace(j$);
+    j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$);
+    j$.ExpectationFilterChain = jRequire.ExpectationFilterChain();
+    j$.Expector = jRequire.Expector(j$);
+    j$.Expectation = jRequire.Expectation(j$);
+    j$.buildExpectationResult = jRequire.buildExpectationResult(j$);
+    j$.JsApiReporter = jRequire.JsApiReporter(j$);
+    j$.asymmetricEqualityTesterArgCompatShim = jRequire.asymmetricEqualityTesterArgCompatShim(
+      j$
+    );
+    j$.makePrettyPrinter = jRequire.makePrettyPrinter(j$);
+    j$.basicPrettyPrinter_ = j$.makePrettyPrinter();
+    Object.defineProperty(j$, 'pp', {
+      get: function() {
+        j$.getEnv().deprecated(
+          'jasmine.pp is deprecated and will be removed in a future release. ' +
+            'Use the pp method of the matchersUtil passed to the matcher factory ' +
+            "or the asymmetric equality tester's `asymmetricMatch` method " +
+            'instead. See ' +
+            '<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#static-utils> for details.'
+        );
+        return j$.basicPrettyPrinter_;
+      }
+    });
+    j$.MatchersUtil = jRequire.MatchersUtil(j$);
+    var staticMatchersUtil = new j$.MatchersUtil({
+      customTesters: [],
+      pp: j$.basicPrettyPrinter_
+    });
+    Object.defineProperty(j$, 'matchersUtil', {
+      get: function() {
+        j$.getEnv().deprecated(
+          'jasmine.matchersUtil is deprecated and will be removed ' +
+            'in a future release. Use the instance passed to the matcher factory or ' +
+            "the asymmetric equality tester's `asymmetricMatch` method instead. " +
+            'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#static-utils> for details.'
+        );
+        return staticMatchersUtil;
+      }
+    });
+
+    j$.ObjectContaining = jRequire.ObjectContaining(j$);
+    j$.ArrayContaining = jRequire.ArrayContaining(j$);
+    j$.ArrayWithExactContents = jRequire.ArrayWithExactContents(j$);
+    j$.MapContaining = jRequire.MapContaining(j$);
+    j$.SetContaining = jRequire.SetContaining(j$);
+    j$.QueueRunner = jRequire.QueueRunner(j$);
+    j$.ReportDispatcher = jRequire.ReportDispatcher(j$);
+    j$.Spec = jRequire.Spec(j$);
+    j$.Spy = jRequire.Spy(j$);
+    j$.SpyFactory = jRequire.SpyFactory(j$);
+    j$.SpyRegistry = jRequire.SpyRegistry(j$);
+    j$.SpyStrategy = jRequire.SpyStrategy(j$);
+    j$.StringMatching = jRequire.StringMatching(j$);
+    j$.StringContaining = jRequire.StringContaining(j$);
+    j$.UserContext = jRequire.UserContext(j$);
+    j$.Suite = jRequire.Suite(j$);
+    j$.Timer = jRequire.Timer();
+    j$.TreeProcessor = jRequire.TreeProcessor();
+    j$.version = jRequire.version();
+    j$.Order = jRequire.Order();
+    j$.DiffBuilder = jRequire.DiffBuilder(j$);
+    j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
+    j$.ObjectPath = jRequire.ObjectPath(j$);
+    j$.MismatchTree = jRequire.MismatchTree(j$);
+    j$.GlobalErrors = jRequire.GlobalErrors(j$);
+
+    j$.Truthy = jRequire.Truthy(j$);
+    j$.Falsy = jRequire.Falsy(j$);
+    j$.Empty = jRequire.Empty(j$);
+    j$.NotEmpty = jRequire.NotEmpty(j$);
+
+    j$.matchers = jRequire.requireMatchers(jRequire, j$);
+    j$.asyncMatchers = jRequire.requireAsyncMatchers(jRequire, j$);
+
+    return j$;
+  };
+
+  return getJasmineRequire;
+})(this);
+
+getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
+  var availableMatchers = [
+      'nothing',
+      'toBe',
+      'toBeCloseTo',
+      'toBeDefined',
+      'toBeInstanceOf',
+      'toBeFalse',
+      'toBeFalsy',
+      'toBeGreaterThan',
+      'toBeGreaterThanOrEqual',
+      'toBeLessThan',
+      'toBeLessThanOrEqual',
+      'toBeNaN',
+      'toBeNegativeInfinity',
+      'toBeNull',
+      'toBePositiveInfinity',
+      'toBeTrue',
+      'toBeTruthy',
+      'toBeUndefined',
+      'toContain',
+      'toEqual',
+      'toHaveSize',
+      'toHaveBeenCalled',
+      'toHaveBeenCalledBefore',
+      'toHaveBeenCalledOnceWith',
+      'toHaveBeenCalledTimes',
+      'toHaveBeenCalledWith',
+      'toHaveClass',
+      'toMatch',
+      'toThrow',
+      'toThrowError',
+      'toThrowMatching'
+    ],
+    matchers = {};
+
+  for (var i = 0; i < availableMatchers.length; i++) {
+    var name = availableMatchers[i];
+    matchers[name] = jRequire[name](j$);
+  }
+
+  return matchers;
+};
+
+getJasmineRequireObj().base = function(j$, jasmineGlobal) {
+  j$.unimplementedMethod_ = function() {
+    throw new Error('unimplemented method');
+  };
+
+  /**
+   * Maximum object depth the pretty printer will print to.
+   * Set this to a lower value to speed up pretty printing if you have large objects.
+   * @name jasmine.MAX_PRETTY_PRINT_DEPTH
+   * @default 8
+   * @since 1.3.0
+   */
+  j$.MAX_PRETTY_PRINT_DEPTH = 8;
+  /**
+   * Maximum number of array elements to display when pretty printing objects.
+   * This will also limit the number of keys and values displayed for an object.
+   * Elements past this number will be ellipised.
+   * @name jasmine.MAX_PRETTY_PRINT_ARRAY_LENGTH
+   * @default 50
+   * @since 2.7.0
+   */
+  j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 50;
+  /**
+   * Maximum number of characters to display when pretty printing objects.
+   * Characters past this number will be ellipised.
+   * @name jasmine.MAX_PRETTY_PRINT_CHARS
+   * @default 100
+   * @since 2.9.0
+   */
+  j$.MAX_PRETTY_PRINT_CHARS = 1000;
+  /**
+   * Default number of milliseconds Jasmine will wait for an asynchronous spec,
+   * before, or after function to complete. This can be overridden on a case by
+   * case basis by passing a time limit as the third argument to {@link it},
+   * {@link beforeEach}, {@link afterEach}, {@link beforeAll}, or
+   * {@link afterAll}. The value must be no greater than the largest number of
+   * milliseconds supported by setTimeout, which is usually 2147483647.
+   *
+   * While debugging tests, you may want to set this to a large number (or pass
+   * a large number to one of the functions mentioned above) so that Jasmine
+   * does not move on to after functions or the next spec while you're debugging.
+   * @name jasmine.DEFAULT_TIMEOUT_INTERVAL
+   * @default 5000
+   * @since 1.3.0
+   */
+  var DEFAULT_TIMEOUT_INTERVAL = 5000;
+  Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', {
+    get: function() {
+      return DEFAULT_TIMEOUT_INTERVAL;
+    },
+    set: function(newValue) {
+      j$.util.validateTimeout(newValue, 'jasmine.DEFAULT_TIMEOUT_INTERVAL');
+      DEFAULT_TIMEOUT_INTERVAL = newValue;
+    }
+  });
+
+  j$.getGlobal = function() {
+    return jasmineGlobal;
+  };
+
+  /**
+   * Get the currently booted Jasmine Environment.
+   *
+   * @name jasmine.getEnv
+   * @since 1.3.0
+   * @function
+   * @return {Env}
+   */
+  j$.getEnv = function(options) {
+    var env = (j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options));
+    //jasmine. singletons in here (setTimeout blah blah).
+    return env;
+  };
+
+  j$.isArray_ = function(value) {
+    return j$.isA_('Array', value);
+  };
+
+  j$.isObject_ = function(value) {
+    return (
+      !j$.util.isUndefined(value) && value !== null && j$.isA_('Object', value)
+    );
+  };
+
+  j$.isString_ = function(value) {
+    return j$.isA_('String', value);
+  };
+
+  j$.isNumber_ = function(value) {
+    return j$.isA_('Number', value);
+  };
+
+  j$.isFunction_ = function(value) {
+    return j$.isA_('Function', value);
+  };
+
+  j$.isAsyncFunction_ = function(value) {
+    return j$.isA_('AsyncFunction', value);
+  };
+
+  j$.isGeneratorFunction_ = function(value) {
+    return j$.isA_('GeneratorFunction', value);
+  };
+
+  j$.isTypedArray_ = function(value) {
+    return (
+      j$.isA_('Float32Array', value) ||
+      j$.isA_('Float64Array', value) ||
+      j$.isA_('Int16Array', value) ||
+      j$.isA_('Int32Array', value) ||
+      j$.isA_('Int8Array', value) ||
+      j$.isA_('Uint16Array', value) ||
+      j$.isA_('Uint32Array', value) ||
+      j$.isA_('Uint8Array', value) ||
+      j$.isA_('Uint8ClampedArray', value)
+    );
+  };
+
+  j$.isA_ = function(typeName, value) {
+    return j$.getType_(value) === '[object ' + typeName + ']';
+  };
+
+  j$.isError_ = function(value) {
+    if (!value) {
+      return false;
+    }
+
+    if (value instanceof Error) {
+      return true;
+    }
+    if (
+      typeof window !== 'undefined' &&
+      typeof window.trustedTypes !== 'undefined'
+    ) {
+      return (
+        typeof value.stack === 'string' && typeof value.message === 'string'
+      );
+    }
+    if (value && value.constructor && value.constructor.constructor) {
+      var valueGlobal = value.constructor.constructor('return this');
+      if (j$.isFunction_(valueGlobal)) {
+        valueGlobal = valueGlobal();
+      }
+
+      if (valueGlobal.Error && value instanceof valueGlobal.Error) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  j$.isAsymmetricEqualityTester_ = function(obj) {
+    return obj ? j$.isA_('Function', obj.asymmetricMatch) : false;
+  };
+
+  j$.getType_ = function(value) {
+    return Object.prototype.toString.apply(value);
+  };
+
+  j$.isDomNode = function(obj) {
+    // Node is a function, because constructors
+    return typeof jasmineGlobal.Node !== 'undefined'
+      ? obj instanceof jasmineGlobal.Node
+      : obj !== null &&
+          typeof obj === 'object' &&
+          typeof obj.nodeType === 'number' &&
+          typeof obj.nodeName === 'string';
+    // return obj.nodeType > 0;
+  };
+
+  j$.isMap = function(obj) {
+    return (
+      obj !== null &&
+      typeof obj !== 'undefined' &&
+      typeof jasmineGlobal.Map !== 'undefined' &&
+      obj.constructor === jasmineGlobal.Map
+    );
+  };
+
+  j$.isSet = function(obj) {
+    return (
+      obj !== null &&
+      typeof obj !== 'undefined' &&
+      typeof jasmineGlobal.Set !== 'undefined' &&
+      obj.constructor === jasmineGlobal.Set
+    );
+  };
+
+  j$.isWeakMap = function(obj) {
+    return (
+      obj !== null &&
+      typeof obj !== 'undefined' &&
+      typeof jasmineGlobal.WeakMap !== 'undefined' &&
+      obj.constructor === jasmineGlobal.WeakMap
+    );
+  };
+
+  j$.isURL = function(obj) {
+    return (
+      obj !== null &&
+      typeof obj !== 'undefined' &&
+      typeof jasmineGlobal.URL !== 'undefined' &&
+      obj.constructor === jasmineGlobal.URL
+    );
+  };
+
+  j$.isDataView = function(obj) {
+    return (
+      obj !== null &&
+      typeof obj !== 'undefined' &&
+      typeof jasmineGlobal.DataView !== 'undefined' &&
+      obj.constructor === jasmineGlobal.DataView
+    );
+  };
+
+  j$.isPromise = function(obj) {
+    return (
+      typeof jasmineGlobal.Promise !== 'undefined' &&
+      !!obj &&
+      obj.constructor === jasmineGlobal.Promise
+    );
+  };
+
+  j$.isPromiseLike = function(obj) {
+    return !!obj && j$.isFunction_(obj.then);
+  };
+
+  j$.fnNameFor = function(func) {
+    if (func.name) {
+      return func.name;
+    }
+
+    var matches =
+      func.toString().match(/^\s*function\s*(\w+)\s*\(/) ||
+      func.toString().match(/^\s*\[object\s*(\w+)Constructor\]/);
+
+    return matches ? matches[1] : '<anonymous>';
+  };
+
+  j$.isPending_ = function(promise) {
+    var sentinel = {};
+    // eslint-disable-next-line compat/compat
+    return Promise.race([promise, Promise.resolve(sentinel)]).then(
+      function(result) {
+        return result === sentinel;
+      },
+      function() {
+        return false;
+      }
+    );
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared is an instance of the specified class/constructor.
+   * @name jasmine.any
+   * @since 1.3.0
+   * @function
+   * @param {Constructor} clazz - The constructor to check against.
+   */
+  j$.any = function(clazz) {
+    return new j$.Any(clazz);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared is not `null` and not `undefined`.
+   * @name jasmine.anything
+   * @since 2.2.0
+   * @function
+   */
+  j$.anything = function() {
+    return new j$.Anything();
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared is `true` or anything truthy.
+   * @name jasmine.truthy
+   * @since 3.1.0
+   * @function
+   */
+  j$.truthy = function() {
+    return new j$.Truthy();
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared is  `null`, `undefined`, `0`, `false` or anything falsey.
+   * @name jasmine.falsy
+   * @since 3.1.0
+   * @function
+   */
+  j$.falsy = function() {
+    return new j$.Falsy();
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared is empty.
+   * @name jasmine.empty
+   * @since 3.1.0
+   * @function
+   */
+  j$.empty = function() {
+    return new j$.Empty();
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared is not empty.
+   * @name jasmine.notEmpty
+   * @since 3.1.0
+   * @function
+   */
+  j$.notEmpty = function() {
+    return new j$.NotEmpty();
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value being compared contains at least the keys and values.
+   * @name jasmine.objectContaining
+   * @since 1.3.0
+   * @function
+   * @param {Object} sample - The subset of properties that _must_ be in the actual.
+   */
+  j$.objectContaining = function(sample) {
+    return new j$.ObjectContaining(sample);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value is a `String` that matches the `RegExp` or `String`.
+   * @name jasmine.stringMatching
+   * @since 2.2.0
+   * @function
+   * @param {RegExp|String} expected
+   */
+  j$.stringMatching = function(expected) {
+    return new j$.StringMatching(expected);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value is a `String` that contains the specified `String`.
+   * @name jasmine.stringContaining
+   * @since 3.10.0
+   * @function
+   * @param {String} expected
+   */
+  j$.stringContaining = function(expected) {
+    return new j$.StringContaining(expected);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value is an `Array` that contains at least the elements in the sample.
+   * @name jasmine.arrayContaining
+   * @since 2.2.0
+   * @function
+   * @param {Array} sample
+   */
+  j$.arrayContaining = function(sample) {
+    return new j$.ArrayContaining(sample);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if the actual value is an `Array` that contains all of the elements in the sample in any order.
+   * @name jasmine.arrayWithExactContents
+   * @since 2.8.0
+   * @function
+   * @param {Array} sample
+   */
+  j$.arrayWithExactContents = function(sample) {
+    return new j$.ArrayWithExactContents(sample);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if every key/value pair in the sample passes the deep equality comparison
+   * with at least one key/value pair in the actual value being compared
+   * @name jasmine.mapContaining
+   * @since 3.5.0
+   * @function
+   * @param {Map} sample - The subset of items that _must_ be in the actual.
+   */
+  j$.mapContaining = function(sample) {
+    return new j$.MapContaining(sample);
+  };
+
+  /**
+   * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+   * that will succeed if every item in the sample passes the deep equality comparison
+   * with at least one item in the actual value being compared
+   * @name jasmine.setContaining
+   * @since 3.5.0
+   * @function
+   * @param {Set} sample - The subset of items that _must_ be in the actual.
+   */
+  j$.setContaining = function(sample) {
+    return new j$.SetContaining(sample);
+  };
+
+  /**
+   * Determines whether the provided function is a Jasmine spy.
+   * @name jasmine.isSpy
+   * @since 2.0.0
+   * @function
+   * @param {Function} putativeSpy - The function to check.
+   * @return {Boolean}
+   */
+  j$.isSpy = function(putativeSpy) {
+    if (!putativeSpy) {
+      return false;
+    }
+    return (
+      putativeSpy.and instanceof j$.SpyStrategy &&
+      putativeSpy.calls instanceof j$.CallTracker
+    );
+  };
+};
+
+getJasmineRequireObj().util = function(j$) {
+  var util = {};
+
+  util.inherit = function(childClass, parentClass) {
+    var Subclass = function() {};
+    Subclass.prototype = parentClass.prototype;
+    childClass.prototype = new Subclass();
+  };
+
+  util.argsToArray = function(args) {
+    var arrayOfArgs = [];
+    for (var i = 0; i < args.length; i++) {
+      arrayOfArgs.push(args[i]);
+    }
+    return arrayOfArgs;
+  };
+
+  util.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  util.arrayContains = function(array, search) {
+    var i = array.length;
+    while (i--) {
+      if (array[i] === search) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  util.clone = function(obj) {
+    if (Object.prototype.toString.apply(obj) === '[object Array]') {
+      return obj.slice();
+    }
+
+    var cloned = {};
+    for (var prop in obj) {
+      if (obj.hasOwnProperty(prop)) {
+        cloned[prop] = obj[prop];
+      }
+    }
+
+    return cloned;
+  };
+
+  util.cloneArgs = function(args) {
+    var clonedArgs = [];
+    var argsAsArray = j$.util.argsToArray(args);
+    for (var i = 0; i < argsAsArray.length; i++) {
+      var str = Object.prototype.toString.apply(argsAsArray[i]),
+        primitives = /^\[object (Boolean|String|RegExp|Number)/;
+
+      // All falsey values are either primitives, `null`, or `undefined.
+      if (!argsAsArray[i] || str.match(primitives)) {
+        clonedArgs.push(argsAsArray[i]);
+      } else {
+        clonedArgs.push(j$.util.clone(argsAsArray[i]));
+      }
+    }
+    return clonedArgs;
+  };
+
+  util.getPropertyDescriptor = function(obj, methodName) {
+    var descriptor,
+      proto = obj;
+
+    do {
+      descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
+      proto = Object.getPrototypeOf(proto);
+    } while (!descriptor && proto);
+
+    return descriptor;
+  };
+
+  util.objectDifference = function(obj, toRemove) {
+    var diff = {};
+
+    for (var key in obj) {
+      if (util.has(obj, key) && !util.has(toRemove, key)) {
+        diff[key] = obj[key];
+      }
+    }
+
+    return diff;
+  };
+
+  util.has = function(obj, key) {
+    return Object.prototype.hasOwnProperty.call(obj, key);
+  };
+
+  util.errorWithStack = function errorWithStack() {
+    // Don't throw and catch if we don't have to, because it makes it harder
+    // for users to debug their code with exception breakpoints.
+    var error = new Error();
+
+    if (error.stack) {
+      return error;
+    }
+
+    // But some browsers (e.g. Phantom) only provide a stack trace if we throw.
+    try {
+      throw new Error();
+    } catch (e) {
+      return e;
+    }
+  };
+
+  function callerFile() {
+    var trace = new j$.StackTrace(util.errorWithStack());
+    return trace.frames[2].file;
+  }
+
+  util.jasmineFile = (function() {
+    var result;
+
+    return function() {
+      if (!result) {
+        result = callerFile();
+      }
+
+      return result;
+    };
+  })();
+
+  function StopIteration() {}
+  StopIteration.prototype = Object.create(Error.prototype);
+  StopIteration.prototype.constructor = StopIteration;
+
+  // useful for maps and sets since `forEach` is the only IE11-compatible way to iterate them
+  util.forEachBreakable = function(iterable, iteratee) {
+    function breakLoop() {
+      throw new StopIteration();
+    }
+
+    try {
+      iterable.forEach(function(value, key) {
+        iteratee(breakLoop, value, key, iterable);
+      });
+    } catch (error) {
+      if (!(error instanceof StopIteration)) throw error;
+    }
+  };
+
+  util.validateTimeout = function(timeout, msgPrefix) {
+    // Timeouts are implemented with setTimeout, which only supports a limited
+    // range of values. The limit is unspecified, as is the behavior when it's
+    // exceeded. But on all currently supported JS runtimes, setTimeout calls
+    // the callback immediately when the timeout is greater than 2147483647
+    // (the maximum value of a signed 32 bit integer).
+    var max = 2147483647;
+
+    if (timeout > max) {
+      throw new Error(
+        (msgPrefix || 'Timeout value') + ' cannot be greater than ' + max
+      );
+    }
+  };
+
+  return util;
+};
+
+getJasmineRequireObj().Spec = function(j$) {
+  /**
+   * @interface Spec
+   * @see Configuration#specFilter
+   * @since 2.0.0
+   */
+  function Spec(attrs) {
+    this.expectationFactory = attrs.expectationFactory;
+    this.asyncExpectationFactory = attrs.asyncExpectationFactory;
+    this.resultCallback = attrs.resultCallback || function() {};
+    /**
+     * The unique ID of this spec.
+     * @name Spec#id
+     * @readonly
+     * @type {string}
+     * @since 2.0.0
+     */
+    this.id = attrs.id;
+    /**
+     * The description passed to the {@link it} that created this spec.
+     * @name Spec#description
+     * @readonly
+     * @type {string}
+     * @since 2.0.0
+     */
+    this.description = attrs.description || '';
+    this.queueableFn = attrs.queueableFn;
+    this.beforeAndAfterFns =
+      attrs.beforeAndAfterFns ||
+      function() {
+        return { befores: [], afters: [] };
+      };
+    this.userContext =
+      attrs.userContext ||
+      function() {
+        return {};
+      };
+    this.onStart = attrs.onStart || function() {};
+    this.autoCleanClosures =
+      attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
+    this.getSpecName =
+      attrs.getSpecName ||
+      function() {
+        return '';
+      };
+    this.expectationResultFactory =
+      attrs.expectationResultFactory || function() {};
+    this.deprecated = attrs.deprecated || function() {};
+    this.queueRunnerFactory = attrs.queueRunnerFactory || function() {};
+    this.catchingExceptions =
+      attrs.catchingExceptions ||
+      function() {
+        return true;
+      };
+    this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
+    this.timer = attrs.timer || new j$.Timer();
+
+    if (!this.queueableFn.fn) {
+      this.exclude();
+    }
+
+    /**
+     * @typedef SpecResult
+     * @property {Int} id - The unique id of this spec.
+     * @property {String} description - The description passed to the {@link it} that created this spec.
+     * @property {String} fullName - The full description including all ancestors of this spec.
+     * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec.
+     * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec.
+     * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
+     * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason.
+     * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec.
+     * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach.
+     * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty}
+     * @since 2.0.0
+x     */
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: [],
+      passedExpectations: [],
+      deprecationWarnings: [],
+      pendingReason: '',
+      duration: null,
+      properties: null
+    };
+  }
+
+  Spec.prototype.addExpectationResult = function(passed, data, isError) {
+    var expectationResult = this.expectationResultFactory(data);
+    if (passed) {
+      this.result.passedExpectations.push(expectationResult);
+    } else {
+      this.result.failedExpectations.push(expectationResult);
+
+      if (this.throwOnExpectationFailure && !isError) {
+        throw new j$.errors.ExpectationFailed();
+      }
+    }
+  };
+
+  Spec.prototype.setSpecProperty = function(key, value) {
+    this.result.properties = this.result.properties || {};
+    this.result.properties[key] = value;
+  };
+
+  Spec.prototype.expect = function(actual) {
+    return this.expectationFactory(actual, this);
+  };
+
+  Spec.prototype.expectAsync = function(actual) {
+    return this.asyncExpectationFactory(actual, this);
+  };
+
+  Spec.prototype.execute = function(onComplete, excluded, failSpecWithNoExp) {
+    var self = this;
+
+    var onStart = {
+      fn: function(done) {
+        self.timer.start();
+        self.onStart(self, done);
+      }
+    };
+
+    var complete = {
+      fn: function(done) {
+        if (self.autoCleanClosures) {
+          self.queueableFn.fn = null;
+        }
+        self.result.status = self.status(excluded, failSpecWithNoExp);
+        self.result.duration = self.timer.elapsed();
+        self.resultCallback(self.result, done);
+      }
+    };
+
+    var fns = this.beforeAndAfterFns();
+    var regularFns = fns.befores.concat(this.queueableFn);
+
+    var runnerConfig = {
+      isLeaf: true,
+      queueableFns: regularFns,
+      cleanupFns: fns.afters,
+      onException: function() {
+        self.onException.apply(self, arguments);
+      },
+      onMultipleDone: function() {
+        // Issue a deprecation. Include the context ourselves and pass
+        // ignoreRunnable: true, since getting here always means that we've already
+        // moved on and the current runnable isn't the one that caused the problem.
+        self.deprecated(
+          "An asynchronous function called its 'done' " +
+            'callback more than once. This is a bug in the spec, beforeAll, ' +
+            'beforeEach, afterAll, or afterEach function in question. This will ' +
+            'be treated as an error in a future version. See' +
+            '<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> ' +
+            'for more information.\n' +
+            '(in spec: ' +
+            self.getFullName() +
+            ')',
+          { ignoreRunnable: true }
+        );
+      },
+      onComplete: function() {
+        if (self.result.status === 'failed') {
+          onComplete(new j$.StopExecutionError('spec failed'));
+        } else {
+          onComplete();
+        }
+      },
+      userContext: this.userContext(),
+      runnableName: this.getFullName.bind(this)
+    };
+
+    if (this.markedPending || excluded === true) {
+      runnerConfig.queueableFns = [];
+      runnerConfig.cleanupFns = [];
+    }
+
+    runnerConfig.queueableFns.unshift(onStart);
+    runnerConfig.cleanupFns.push(complete);
+
+    this.queueRunnerFactory(runnerConfig);
+  };
+
+  Spec.prototype.reset = function() {
+    /**
+     * @typedef SpecResult
+     * @property {Int} id - The unique id of this spec.
+     * @property {String} description - The description passed to the {@link it} that created this spec.
+     * @property {String} fullName - The full description including all ancestors of this spec.
+     * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec.
+     * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec.
+     * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
+     * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason.
+     * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec.
+     * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach.
+     * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty}
+     * @since 2.0.0
+     */
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: [],
+      passedExpectations: [],
+      deprecationWarnings: [],
+      pendingReason: this.excludeMessage,
+      duration: null,
+      properties: null,
+      trace: null
+    };
+    this.markedPending = this.markedExcluding;
+  };
+
+  Spec.prototype.onException = function onException(e) {
+    if (Spec.isPendingSpecException(e)) {
+      this.pend(extractCustomPendingMessage(e));
+      return;
+    }
+
+    if (e instanceof j$.errors.ExpectationFailed) {
+      return;
+    }
+
+    this.addExpectationResult(
+      false,
+      {
+        matcherName: '',
+        passed: false,
+        expected: '',
+        actual: '',
+        error: e
+      },
+      true
+    );
+  };
+
+  /*
+   * Marks state as pending
+   * @param {string} [message] An optional reason message
+   */
+  Spec.prototype.pend = function(message) {
+    this.markedPending = true;
+    if (message) {
+      this.result.pendingReason = message;
+    }
+  };
+
+  /*
+   * Like {@link Spec#pend}, but pending state will survive {@link Spec#reset}
+   * Useful for fit, xit, where pending state remains.
+   * @param {string} [message] An optional reason message
+   */
+  Spec.prototype.exclude = function(message) {
+    this.markedExcluding = true;
+    if (this.message) {
+      this.excludeMessage = message;
+    }
+    this.pend(message);
+  };
+
+  Spec.prototype.getResult = function() {
+    this.result.status = this.status();
+    return this.result;
+  };
+
+  Spec.prototype.status = function(excluded, failSpecWithNoExpectations) {
+    if (excluded === true) {
+      return 'excluded';
+    }
+
+    if (this.markedPending) {
+      return 'pending';
+    }
+
+    if (
+      this.result.failedExpectations.length > 0 ||
+      (failSpecWithNoExpectations &&
+        this.result.failedExpectations.length +
+          this.result.passedExpectations.length ===
+          0)
+    ) {
+      return 'failed';
+    }
+
+    return 'passed';
+  };
+
+  /**
+   * The full description including all ancestors of this spec.
+   * @name Spec#getFullName
+   * @function
+   * @returns {string}
+   * @since 2.0.0
+   */
+  Spec.prototype.getFullName = function() {
+    return this.getSpecName(this);
+  };
+
+  Spec.prototype.addDeprecationWarning = function(deprecation) {
+    if (typeof deprecation === 'string') {
+      deprecation = { message: deprecation };
+    }
+    this.result.deprecationWarnings.push(
+      this.expectationResultFactory(deprecation)
+    );
+  };
+
+  var extractCustomPendingMessage = function(e) {
+    var fullMessage = e.toString(),
+      boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
+      boilerplateEnd =
+        boilerplateStart + Spec.pendingSpecExceptionMessage.length;
+
+    return fullMessage.substr(boilerplateEnd);
+  };
+
+  Spec.pendingSpecExceptionMessage = '=> marked Pending';
+
+  Spec.isPendingSpecException = function(e) {
+    return !!(
+      e &&
+      e.toString &&
+      e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1
+    );
+  };
+
+  return Spec;
+};
+
+if (typeof window == void 0 && typeof exports == 'object') {
+  /* globals exports */
+  exports.Spec = jasmineRequire.Spec;
+}
+
+/*jshint bitwise: false*/
+
+getJasmineRequireObj().Order = function() {
+  function Order(options) {
+    this.random = 'random' in options ? options.random : true;
+    var seed = (this.seed = options.seed || generateSeed());
+    this.sort = this.random ? randomOrder : naturalOrder;
+
+    function naturalOrder(items) {
+      return items;
+    }
+
+    function randomOrder(items) {
+      var copy = items.slice();
+      copy.sort(function(a, b) {
+        return jenkinsHash(seed + a.id) - jenkinsHash(seed + b.id);
+      });
+      return copy;
+    }
+
+    function generateSeed() {
+      return String(Math.random()).slice(-5);
+    }
+
+    // Bob Jenkins One-at-a-Time Hash algorithm is a non-cryptographic hash function
+    // used to get a different output when the key changes slightly.
+    // We use your return to sort the children randomly in a consistent way when
+    // used in conjunction with a seed
+
+    function jenkinsHash(key) {
+      var hash, i;
+      for (hash = i = 0; i < key.length; ++i) {
+        hash += key.charCodeAt(i);
+        hash += hash << 10;
+        hash ^= hash >> 6;
+      }
+      hash += hash << 3;
+      hash ^= hash >> 11;
+      hash += hash << 15;
+      return hash;
+    }
+  }
+
+  return Order;
+};
+
+getJasmineRequireObj().Env = function(j$) {
+  /**
+   * @class Env
+   * @since 2.0.0
+   * @classdesc The Jasmine environment.<br>
+   * _Note:_ Do not construct this directly. You can obtain the Env instance by
+   * calling {@link jasmine.getEnv}.
+   * @hideconstructor
+   */
+  function Env(options) {
+    options = options || {};
+
+    var self = this;
+    var global = options.global || j$.getGlobal();
+    var customPromise;
+
+    var totalSpecsDefined = 0;
+
+    var realSetTimeout = global.setTimeout;
+    var realClearTimeout = global.clearTimeout;
+    var clearStack = j$.getClearStack(global);
+    this.clock = new j$.Clock(
+      global,
+      function() {
+        return new j$.DelayedFunctionScheduler();
+      },
+      new j$.MockDate(global)
+    );
+
+    var runnableResources = {};
+
+    var currentSpec = null;
+    var currentlyExecutingSuites = [];
+    var currentDeclarationSuite = null;
+    var hasFailures = false;
+
+    /**
+     * This represents the available options to configure Jasmine.
+     * Options that are not provided will use their default values.
+     * @see Env#configure
+     * @interface Configuration
+     * @since 3.3.0
+     */
+    var config = {
+      /**
+       * Whether to randomize spec execution order
+       * @name Configuration#random
+       * @since 3.3.0
+       * @type Boolean
+       * @default true
+       */
+      random: true,
+      /**
+       * Seed to use as the basis of randomization.
+       * Null causes the seed to be determined randomly at the start of execution.
+       * @name Configuration#seed
+       * @since 3.3.0
+       * @type (number|string)
+       * @default null
+       */
+      seed: null,
+      /**
+       * Whether to stop execution of the suite after the first spec failure
+       * @name Configuration#failFast
+       * @since 3.3.0
+       * @type Boolean
+       * @default false
+       * @deprecated Use the `stopOnSpecFailure` config property instead.
+       */
+      failFast: false,
+      /**
+       * Whether to stop execution of the suite after the first spec failure
+       * @name Configuration#stopOnSpecFailure
+       * @since 3.9.0
+       * @type Boolean
+       * @default false
+       */
+      stopOnSpecFailure: false,
+      /**
+       * Whether to fail the spec if it ran no expectations. By default
+       * a spec that ran no expectations is reported as passed. Setting this
+       * to true will report such spec as a failure.
+       * @name Configuration#failSpecWithNoExpectations
+       * @since 3.5.0
+       * @type Boolean
+       * @default false
+       */
+      failSpecWithNoExpectations: false,
+      /**
+       * Whether to cause specs to only have one expectation failure.
+       * @name Configuration#oneFailurePerSpec
+       * @since 3.3.0
+       * @type Boolean
+       * @default false
+       * @deprecated Use the `stopSpecOnExpectationFailure` config property instead.
+       */
+      oneFailurePerSpec: false,
+      /**
+       * Whether to cause specs to only have one expectation failure.
+       * @name Configuration#stopSpecOnExpectationFailure
+       * @since 3.3.0
+       * @type Boolean
+       * @default false
+       */
+      stopSpecOnExpectationFailure: false,
+      /**
+       * A function that takes a spec and returns true if it should be executed
+       * or false if it should be skipped.
+       * @callback SpecFilter
+       * @param {Spec} spec - The spec that the filter is being applied to.
+       * @return boolean
+       */
+      /**
+       * Function to use to filter specs
+       * @name Configuration#specFilter
+       * @since 3.3.0
+       * @type SpecFilter
+       * @default A function that always returns true.
+       */
+      specFilter: function() {
+        return true;
+      },
+      /**
+       * Whether or not reporters should hide disabled specs from their output.
+       * Currently only supported by Jasmine's HTMLReporter
+       * @name Configuration#hideDisabled
+       * @since 3.3.0
+       * @type Boolean
+       * @default false
+       */
+      hideDisabled: false,
+      /**
+       * Set to provide a custom promise library that Jasmine will use if it needs
+       * to create a promise. If not set, it will default to whatever global Promise
+       * library is available (if any).
+       * @name Configuration#Promise
+       * @since 3.5.0
+       * @type function
+       * @default undefined
+       * @deprecated In a future version, Jasmine will ignore the Promise config
+       * property and always create native promises instead.
+       */
+      Promise: undefined,
+      /**
+       * Clean closures when a suite is done running (done by clearing the stored function reference).
+       * This prevents memory leaks, but you won't be able to run jasmine multiple times.
+       * @name Configuration#autoCleanClosures
+       * @since 3.10.0
+       * @type boolean
+       * @default true
+       */
+      autoCleanClosures: true,
+      /**
+       * Whether or not to issue warnings for certain deprecated functionality
+       * every time it's used. If not set or set to false, deprecation warnings
+       * for methods that tend to be called frequently will be issued only once
+       * or otherwise throttled to to prevent the suite output from being flooded
+       * with warnings.
+       * @name Configuration#verboseDeprecations
+       * @since 3.6.0
+       * @type Boolean
+       * @default false
+       */
+      verboseDeprecations: false
+    };
+
+    var currentSuite = function() {
+      return currentlyExecutingSuites[currentlyExecutingSuites.length - 1];
+    };
+
+    var currentRunnable = function() {
+      return currentSpec || currentSuite();
+    };
+
+    var globalErrors = null;
+
+    var installGlobalErrors = function() {
+      if (globalErrors) {
+        return;
+      }
+
+      globalErrors = new j$.GlobalErrors();
+      globalErrors.install();
+    };
+
+    if (!options.suppressLoadErrors) {
+      installGlobalErrors();
+      globalErrors.pushListener(function(
+        message,
+        filename,
+        lineno,
+        colNo,
+        err
+      ) {
+        topSuite.result.failedExpectations.push({
+          passed: false,
+          globalErrorType: 'load',
+          message: message,
+          stack: err && err.stack,
+          filename: filename,
+          lineno: lineno
+        });
+      });
+    }
+
+    /**
+     * Configure your jasmine environment
+     * @name Env#configure
+     * @since 3.3.0
+     * @argument {Configuration} configuration
+     * @function
+     */
+    this.configure = function(configuration) {
+      var booleanProps = [
+        'random',
+        'failSpecWithNoExpectations',
+        'hideDisabled',
+        'autoCleanClosures'
+      ];
+
+      booleanProps.forEach(function(prop) {
+        if (typeof configuration[prop] !== 'undefined') {
+          config[prop] = !!configuration[prop];
+        }
+      });
+
+      if (typeof configuration.failFast !== 'undefined') {
+        // We can't unconditionally issue a warning here because then users who
+        // get the configuration from Jasmine, modify it, and pass it back would
+        // see the warning.
+        if (configuration.failFast !== config.failFast) {
+          this.deprecated(
+            'The `failFast` config property is deprecated and will be removed ' +
+              'in a future version of Jasmine. Please use `stopOnSpecFailure` ' +
+              'instead.',
+            { ignoreRunnable: true }
+          );
+        }
+
+        if (typeof configuration.stopOnSpecFailure !== 'undefined') {
+          if (configuration.stopOnSpecFailure !== configuration.failFast) {
+            throw new Error(
+              'stopOnSpecFailure and failFast are aliases for ' +
+                "each other. Don't set failFast if you also set stopOnSpecFailure."
+            );
+          }
+        }
+
+        config.failFast = configuration.failFast;
+        config.stopOnSpecFailure = configuration.failFast;
+      } else if (typeof configuration.stopOnSpecFailure !== 'undefined') {
+        config.failFast = configuration.stopOnSpecFailure;
+        config.stopOnSpecFailure = configuration.stopOnSpecFailure;
+      }
+
+      if (typeof configuration.oneFailurePerSpec !== 'undefined') {
+        // We can't unconditionally issue a warning here because then users who
+        // get the configuration from Jasmine, modify it, and pass it back would
+        // see the warning.
+        if (configuration.oneFailurePerSpec !== config.oneFailurePerSpec) {
+          this.deprecated(
+            'The `oneFailurePerSpec` config property is deprecated and will be ' +
+              'removed in a future version of Jasmine. Please use ' +
+              '`stopSpecOnExpectationFailure` instead.',
+            { ignoreRunnable: true }
+          );
+        }
+
+        if (typeof configuration.stopSpecOnExpectationFailure !== 'undefined') {
+          if (
+            configuration.stopSpecOnExpectationFailure !==
+            configuration.oneFailurePerSpec
+          ) {
+            throw new Error(
+              'stopSpecOnExpectationFailure and oneFailurePerSpec are aliases for ' +
+                "each other. Don't set oneFailurePerSpec if you also set stopSpecOnExpectationFailure."
+            );
+          }
+        }
+
+        config.oneFailurePerSpec = configuration.oneFailurePerSpec;
+        config.stopSpecOnExpectationFailure = configuration.oneFailurePerSpec;
+      } else if (
+        typeof configuration.stopSpecOnExpectationFailure !== 'undefined'
+      ) {
+        config.oneFailurePerSpec = configuration.stopSpecOnExpectationFailure;
+        config.stopSpecOnExpectationFailure =
+          configuration.stopSpecOnExpectationFailure;
+      }
+
+      if (configuration.specFilter) {
+        config.specFilter = configuration.specFilter;
+      }
+
+      if (typeof configuration.seed !== 'undefined') {
+        config.seed = configuration.seed;
+      }
+
+      // Don't use hasOwnProperty to check for Promise existence because Promise
+      // can be initialized to undefined, either explicitly or by using the
+      // object returned from Env#configuration. In particular, Karma does this.
+      if (configuration.Promise) {
+        if (
+          typeof configuration.Promise.resolve === 'function' &&
+          typeof configuration.Promise.reject === 'function'
+        ) {
+          customPromise = configuration.Promise;
+          self.deprecated(
+            'The `Promise` config property is deprecated. Future versions ' +
+              'of Jasmine will create native promises even if the `Promise` ' +
+              'config property is set. Please remove it.'
+          );
+        } else {
+          throw new Error(
+            'Custom promise library missing `resolve`/`reject` functions'
+          );
+        }
+      }
+
+      if (configuration.hasOwnProperty('verboseDeprecations')) {
+        config.verboseDeprecations = configuration.verboseDeprecations;
+        deprecator.verboseDeprecations(config.verboseDeprecations);
+      }
+    };
+
+    /**
+     * Get the current configuration for your jasmine environment
+     * @name Env#configuration
+     * @since 3.3.0
+     * @function
+     * @returns {Configuration}
+     */
+    this.configuration = function() {
+      var result = {};
+      for (var property in config) {
+        result[property] = config[property];
+      }
+      return result;
+    };
+
+    Object.defineProperty(this, 'specFilter', {
+      get: function() {
+        self.deprecated(
+          'Getting specFilter directly from Env is deprecated and will be ' +
+            'removed in a future version of Jasmine. Please check the ' +
+            'specFilter option from `configuration` instead.',
+          { ignoreRunnable: true }
+        );
+        return config.specFilter;
+      },
+      set: function(val) {
+        self.deprecated(
+          'Setting specFilter directly on Env is deprecated and will be ' +
+            'removed in a future version of Jasmine. Please use the ' +
+            'specFilter option in `configure` instead.',
+          { ignoreRunnable: true }
+        );
+        config.specFilter = val;
+      }
+    });
+
+    this.setDefaultSpyStrategy = function(defaultStrategyFn) {
+      if (!currentRunnable()) {
+        throw new Error(
+          'Default spy strategy must be set in a before function or a spec'
+        );
+      }
+      runnableResources[
+        currentRunnable().id
+      ].defaultStrategyFn = defaultStrategyFn;
+    };
+
+    this.addSpyStrategy = function(name, fn) {
+      if (!currentRunnable()) {
+        throw new Error(
+          'Custom spy strategies must be added in a before function or a spec'
+        );
+      }
+      runnableResources[currentRunnable().id].customSpyStrategies[name] = fn;
+    };
+
+    this.addCustomEqualityTester = function(tester) {
+      if (!currentRunnable()) {
+        throw new Error(
+          'Custom Equalities must be added in a before function or a spec'
+        );
+      }
+      runnableResources[currentRunnable().id].customEqualityTesters.push(
+        tester
+      );
+    };
+
+    this.addMatchers = function(matchersToAdd) {
+      if (!currentRunnable()) {
+        throw new Error(
+          'Matchers must be added in a before function or a spec'
+        );
+      }
+      var customMatchers =
+        runnableResources[currentRunnable().id].customMatchers;
+
+      for (var matcherName in matchersToAdd) {
+        if (matchersToAdd[matcherName].length > 1) {
+          self.deprecated(
+            'The matcher factory for "' +
+              matcherName +
+              '" ' +
+              'accepts custom equality testers, but this parameter will no longer be ' +
+              'passed in a future release. ' +
+              'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#matchers-cet> for details.'
+          );
+        }
+
+        customMatchers[matcherName] = matchersToAdd[matcherName];
+      }
+    };
+
+    this.addAsyncMatchers = function(matchersToAdd) {
+      if (!currentRunnable()) {
+        throw new Error(
+          'Async Matchers must be added in a before function or a spec'
+        );
+      }
+      var customAsyncMatchers =
+        runnableResources[currentRunnable().id].customAsyncMatchers;
+
+      for (var matcherName in matchersToAdd) {
+        if (matchersToAdd[matcherName].length > 1) {
+          self.deprecated(
+            'The matcher factory for "' +
+              matcherName +
+              '" ' +
+              'accepts custom equality testers, but this parameter will no longer be ' +
+              'passed in a future release. ' +
+              'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#matchers-cet> for details.'
+          );
+        }
+
+        customAsyncMatchers[matcherName] = matchersToAdd[matcherName];
+      }
+    };
+
+    this.addCustomObjectFormatter = function(formatter) {
+      if (!currentRunnable()) {
+        throw new Error(
+          'Custom object formatters must be added in a before function or a spec'
+        );
+      }
+
+      runnableResources[currentRunnable().id].customObjectFormatters.push(
+        formatter
+      );
+    };
+
+    j$.Expectation.addCoreMatchers(j$.matchers);
+    j$.Expectation.addAsyncCoreMatchers(j$.asyncMatchers);
+
+    var nextSpecId = 0;
+    var getNextSpecId = function() {
+      return 'spec' + nextSpecId++;
+    };
+
+    var nextSuiteId = 0;
+    var getNextSuiteId = function() {
+      return 'suite' + nextSuiteId++;
+    };
+
+    var makePrettyPrinter = function() {
+      var customObjectFormatters =
+        runnableResources[currentRunnable().id].customObjectFormatters;
+      return j$.makePrettyPrinter(customObjectFormatters);
+    };
+
+    var makeMatchersUtil = function() {
+      var customEqualityTesters =
+        runnableResources[currentRunnable().id].customEqualityTesters;
+      return new j$.MatchersUtil({
+        customTesters: customEqualityTesters,
+        pp: makePrettyPrinter()
+      });
+    };
+
+    var expectationFactory = function(actual, spec) {
+      var customEqualityTesters =
+        runnableResources[spec.id].customEqualityTesters;
+
+      return j$.Expectation.factory({
+        matchersUtil: makeMatchersUtil(),
+        customEqualityTesters: customEqualityTesters,
+        customMatchers: runnableResources[spec.id].customMatchers,
+        actual: actual,
+        addExpectationResult: addExpectationResult
+      });
+
+      function addExpectationResult(passed, result) {
+        return spec.addExpectationResult(passed, result);
+      }
+    };
+
+    function recordLateExpectation(runable, runableType, result) {
+      var delayedExpectationResult = {};
+      Object.keys(result).forEach(function(k) {
+        delayedExpectationResult[k] = result[k];
+      });
+      delayedExpectationResult.passed = false;
+      delayedExpectationResult.globalErrorType = 'lateExpectation';
+      delayedExpectationResult.message =
+        runableType +
+        ' "' +
+        runable.getFullName() +
+        '" ran a "' +
+        result.matcherName +
+        '" expectation after it finished.\n';
+
+      if (result.message) {
+        delayedExpectationResult.message +=
+          'Message: "' + result.message + '"\n';
+      }
+
+      delayedExpectationResult.message +=
+        '1. Did you forget to return or await the result of expectAsync?\n' +
+        '2. Was done() invoked before an async operation completed?\n' +
+        '3. Did an expectation follow a call to done()?';
+
+      topSuite.result.failedExpectations.push(delayedExpectationResult);
+    }
+
+    var asyncExpectationFactory = function(actual, spec, runableType) {
+      return j$.Expectation.asyncFactory({
+        matchersUtil: makeMatchersUtil(),
+        customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
+        customAsyncMatchers: runnableResources[spec.id].customAsyncMatchers,
+        actual: actual,
+        addExpectationResult: addExpectationResult
+      });
+
+      function addExpectationResult(passed, result) {
+        if (currentRunnable() !== spec) {
+          recordLateExpectation(spec, runableType, result);
+        }
+        return spec.addExpectationResult(passed, result);
+      }
+    };
+    var suiteAsyncExpectationFactory = function(actual, suite) {
+      return asyncExpectationFactory(actual, suite, 'Suite');
+    };
+
+    var specAsyncExpectationFactory = function(actual, suite) {
+      return asyncExpectationFactory(actual, suite, 'Spec');
+    };
+
+    var defaultResourcesForRunnable = function(id, parentRunnableId) {
+      var resources = {
+        spies: [],
+        customEqualityTesters: [],
+        customMatchers: {},
+        customAsyncMatchers: {},
+        customSpyStrategies: {},
+        defaultStrategyFn: undefined,
+        customObjectFormatters: []
+      };
+
+      if (runnableResources[parentRunnableId]) {
+        resources.customEqualityTesters = j$.util.clone(
+          runnableResources[parentRunnableId].customEqualityTesters
+        );
+        resources.customMatchers = j$.util.clone(
+          runnableResources[parentRunnableId].customMatchers
+        );
+        resources.customAsyncMatchers = j$.util.clone(
+          runnableResources[parentRunnableId].customAsyncMatchers
+        );
+        resources.customObjectFormatters = j$.util.clone(
+          runnableResources[parentRunnableId].customObjectFormatters
+        );
+        resources.defaultStrategyFn =
+          runnableResources[parentRunnableId].defaultStrategyFn;
+      }
+
+      runnableResources[id] = resources;
+    };
+
+    var clearResourcesForRunnable = function(id) {
+      spyRegistry.clearSpies();
+      delete runnableResources[id];
+    };
+
+    var beforeAndAfterFns = function(targetSuite) {
+      return function() {
+        var befores = [],
+          afters = [],
+          suite = targetSuite;
+
+        while (suite) {
+          befores = befores.concat(suite.beforeFns);
+          afters = afters.concat(suite.afterFns);
+
+          suite = suite.parentSuite;
+        }
+
+        return {
+          befores: befores.reverse(),
+          afters: afters
+        };
+      };
+    };
+
+    var getSpecName = function(spec, suite) {
+      var fullName = [spec.description],
+        suiteFullName = suite.getFullName();
+
+      if (suiteFullName !== '') {
+        fullName.unshift(suiteFullName);
+      }
+      return fullName.join(' ');
+    };
+
+    // TODO: we may just be able to pass in the fn instead of wrapping here
+    var buildExpectationResult = j$.buildExpectationResult,
+      exceptionFormatter = new j$.ExceptionFormatter(),
+      expectationResultFactory = function(attrs) {
+        attrs.messageFormatter = exceptionFormatter.message;
+        attrs.stackFormatter = exceptionFormatter.stack;
+
+        return buildExpectationResult(attrs);
+      };
+
+    /**
+     * Sets whether Jasmine should throw an Error when an expectation fails.
+     * This causes a spec to only have one expectation failure.
+     * @name Env#throwOnExpectationFailure
+     * @since 2.3.0
+     * @function
+     * @param {Boolean} value Whether to throw when a expectation fails
+     * @deprecated Use the `stopSpecOnExpectationFailure` option with {@link Env#configure}
+     */
+    this.throwOnExpectationFailure = function(value) {
+      this.deprecated(
+        'Setting throwOnExpectationFailure directly on Env is deprecated and ' +
+          'will be removed in a future version of Jasmine. Please use the ' +
+          'stopSpecOnExpectationFailure option in `configure`.',
+        { ignoreRunnable: true }
+      );
+      this.configure({ oneFailurePerSpec: !!value });
+    };
+
+    this.throwingExpectationFailures = function() {
+      this.deprecated(
+        'Getting throwingExpectationFailures directly from Env is deprecated ' +
+          'and will be removed in a future version of Jasmine. Please check ' +
+          'the stopSpecOnExpectationFailure option from `configuration`.',
+        { ignoreRunnable: true }
+      );
+      return config.oneFailurePerSpec;
+    };
+
+    /**
+     * Set whether to stop suite execution when a spec fails
+     * @name Env#stopOnSpecFailure
+     * @since 2.7.0
+     * @function
+     * @param {Boolean} value Whether to stop suite execution when a spec fails
+     * @deprecated Use the `stopOnSpecFailure` option with {@link Env#configure}
+     */
+    this.stopOnSpecFailure = function(value) {
+      this.deprecated(
+        'Setting stopOnSpecFailure directly is deprecated and will be ' +
+          'removed in a future version of Jasmine. Please use the ' +
+          'stopOnSpecFailure option in `configure`.',
+        { ignoreRunnable: true }
+      );
+      this.configure({ stopOnSpecFailure: !!value });
+    };
+
+    this.stoppingOnSpecFailure = function() {
+      this.deprecated(
+        'Getting stoppingOnSpecFailure directly from Env is deprecated and ' +
+          'will be removed in a future version of Jasmine. Please check the ' +
+          'stopOnSpecFailure option from `configuration`.',
+        { ignoreRunnable: true }
+      );
+      return config.failFast;
+    };
+
+    /**
+     * Set whether to randomize test execution order
+     * @name Env#randomizeTests
+     * @since 2.4.0
+     * @function
+     * @param {Boolean} value Whether to randomize execution order
+     * @deprecated Use the `random` option with {@link Env#configure}
+     */
+    this.randomizeTests = function(value) {
+      this.deprecated(
+        'Setting randomizeTests directly is deprecated and will be removed ' +
+          'in a future version of Jasmine. Please use the random option in ' +
+          '`configure` instead.',
+        { ignoreRunnable: true }
+      );
+      config.random = !!value;
+    };
+
+    this.randomTests = function() {
+      this.deprecated(
+        'Getting randomTests directly from Env is deprecated and will be ' +
+          'removed in a future version of Jasmine. Please check the random ' +
+          'option from `configuration` instead.',
+        { ignoreRunnable: true }
+      );
+      return config.random;
+    };
+
+    /**
+     * Set the random number seed for spec randomization
+     * @name Env#seed
+     * @since 2.4.0
+     * @function
+     * @param {Number} value The seed value
+     * @deprecated Use the `seed` option with {@link Env#configure}
+     */
+    this.seed = function(value) {
+      this.deprecated(
+        'Setting seed directly is deprecated and will be removed in a ' +
+          'future version of Jasmine. Please use the seed option in ' +
+          '`configure` instead.',
+        { ignoreRunnable: true }
+      );
+      if (value) {
+        config.seed = value;
+      }
+      return config.seed;
+    };
+
+    this.hidingDisabled = function(value) {
+      this.deprecated(
+        'Getting hidingDisabled directly from Env is deprecated and will ' +
+          'be removed in a future version of Jasmine. Please check the ' +
+          'hideDisabled option from `configuration` instead.',
+        { ignoreRunnable: true }
+      );
+      return config.hideDisabled;
+    };
+
+    /**
+     * @name Env#hideDisabled
+     * @since 3.2.0
+     * @function
+     * @deprecated Use the `hideDisabled` option with {@link Env#configure}
+     */
+    this.hideDisabled = function(value) {
+      this.deprecated(
+        'Setting hideDisabled directly is deprecated and will be removed ' +
+          'in a future version of Jasmine. Please use the hideDisabled option ' +
+          'in `configure` instead.',
+        { ignoreRunnable: true }
+      );
+      config.hideDisabled = !!value;
+    };
+
+    /**
+     * Causes a deprecation warning to be logged to the console and reported to
+     * reporters.
+     *
+     * The optional second parameter is an object that can have either of the
+     * following properties:
+     *
+     * omitStackTrace: Whether to omit the stack trace. Optional. Defaults to
+     * false. This option is ignored if the deprecation is an Error. Set this
+     * when the stack trace will not contain anything that helps the user find
+     * the source of the deprecation.
+     *
+     * ignoreRunnable: Whether to log the deprecation on the root suite, ignoring
+     * the spec or suite that's running when it happens. Optional. Defaults to
+     * false.
+     *
+     * @name Env#deprecated
+     * @since 2.99
+     * @function
+     * @param {String|Error} deprecation The deprecation message
+     * @param {Object} [options] Optional extra options, as described above
+     */
+    this.deprecated = function(deprecation, options) {
+      var runnable = currentRunnable() || topSuite;
+      deprecator.addDeprecationWarning(runnable, deprecation, options);
+    };
+
+    var queueRunnerFactory = function(options, args) {
+      var failFast = false;
+      if (options.isLeaf) {
+        failFast = config.stopSpecOnExpectationFailure;
+      } else if (!options.isReporter) {
+        failFast = config.stopOnSpecFailure;
+      }
+      options.clearStack = options.clearStack || clearStack;
+      options.timeout = {
+        setTimeout: realSetTimeout,
+        clearTimeout: realClearTimeout
+      };
+      options.fail = self.fail;
+      options.globalErrors = globalErrors;
+      options.completeOnFirstError = failFast;
+      options.onException =
+        options.onException ||
+        function(e) {
+          (currentRunnable() || topSuite).onException(e);
+        };
+      options.deprecated = self.deprecated;
+
+      new j$.QueueRunner(options).execute(args);
+    };
+
+    var topSuite = new j$.Suite({
+      env: this,
+      id: getNextSuiteId(),
+      description: 'Jasmine__TopLevel__Suite',
+      expectationFactory: expectationFactory,
+      asyncExpectationFactory: suiteAsyncExpectationFactory,
+      expectationResultFactory: expectationResultFactory,
+      autoCleanClosures: config.autoCleanClosures
+    });
+    var deprecator = new j$.Deprecator(topSuite);
+    currentDeclarationSuite = topSuite;
+
+    /**
+     * Provides the root suite, through which all suites and specs can be
+     * accessed.
+     * @function
+     * @name Env#topSuite
+     * @return {Suite} the root suite
+     * @since 2.0.0
+     */
+    this.topSuite = function() {
+      return j$.deprecatingSuiteProxy(topSuite, null, this);
+    };
+
+    /**
+     * This represents the available reporter callback for an object passed to {@link Env#addReporter}.
+     * @interface Reporter
+     * @see custom_reporter
+     */
+    var reporter = new j$.ReportDispatcher(
+      [
+        /**
+         * `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.
+         * @function
+         * @name Reporter#jasmineStarted
+         * @param {JasmineStartedInfo} suiteInfo Information about the full Jasmine suite that is being run
+         * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+         * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+         * @see async
+         */
+        'jasmineStarted',
+        /**
+         * When the entire suite has finished execution `jasmineDone` is called
+         * @function
+         * @name Reporter#jasmineDone
+         * @param {JasmineDoneInfo} suiteInfo Information about the full Jasmine suite that just finished running.
+         * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+         * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+         * @see async
+         */
+        'jasmineDone',
+        /**
+         * `suiteStarted` is invoked when a `describe` starts to run
+         * @function
+         * @name Reporter#suiteStarted
+         * @param {SuiteResult} result Information about the individual {@link describe} being run
+         * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+         * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+         * @see async
+         */
+        'suiteStarted',
+        /**
+         * `suiteDone` is invoked when all of the child specs and suites for a given suite have been run
+         *
+         * While jasmine doesn't require any specific functions, not defining a `suiteDone` will make it impossible for a reporter to know when a suite has failures in an `afterAll`.
+         * @function
+         * @name Reporter#suiteDone
+         * @param {SuiteResult} result
+         * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+         * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+         * @see async
+         */
+        'suiteDone',
+        /**
+         * `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
+         * @function
+         * @name Reporter#specStarted
+         * @param {SpecResult} result Information about the individual {@link it} being run
+         * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+         * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+         * @see async
+         */
+        'specStarted',
+        /**
+         * `specDone` is invoked when an `it` and its associated `beforeEach` and `afterEach` functions have been run.
+         *
+         * While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed.
+         * @function
+         * @name Reporter#specDone
+         * @param {SpecResult} result
+         * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+         * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+         * @see async
+         */
+        'specDone'
+      ],
+      queueRunnerFactory,
+      self.deprecated
+    );
+
+    /**
+     * Executes the specs.
+     *
+     * If called with no parameters or with a falsy value as the first parameter,
+     * all specs will be executed except those that are excluded by a
+     * [spec filter]{@link Configuration#specFilter} or other mechanism. If the
+     * first parameter is a list of spec/suite IDs, only those specs/suites will
+     * be run.
+     *
+     * Both parameters are optional, but a completion callback is only valid as
+     * the second parameter. To specify a completion callback but not a list of
+     * specs/suites to run, pass null or undefined as the first parameter.
+     *
+     * execute should not be called more than once.
+     *
+     * If the environment supports promises, execute will return a promise that
+     * is resolved after the suite finishes executing. The promise will be
+     * resolved (not rejected) as long as the suite runs to completion. Use a
+     * {@link Reporter} to determine whether or not the suite passed.
+     *
+     * @name Env#execute
+     * @since 2.0.0
+     * @function
+     * @param {(string[])=} runnablesToRun IDs of suites and/or specs to run
+     * @param {Function=} onComplete Function that will be called after all specs have run
+     * @return {Promise<undefined>}
+     */
+    this.execute = function(runnablesToRun, onComplete) {
+      if (this._executedBefore) {
+        topSuite.reset();
+      }
+      this._executedBefore = true;
+      defaultResourcesForRunnable(topSuite.id);
+      installGlobalErrors();
+
+      if (!runnablesToRun) {
+        if (focusedRunnables.length) {
+          runnablesToRun = focusedRunnables;
+        } else {
+          runnablesToRun = [topSuite.id];
+        }
+      }
+
+      var order = new j$.Order({
+        random: config.random,
+        seed: config.seed
+      });
+
+      var processor = new j$.TreeProcessor({
+        tree: topSuite,
+        runnableIds: runnablesToRun,
+        queueRunnerFactory: queueRunnerFactory,
+        failSpecWithNoExpectations: config.failSpecWithNoExpectations,
+        nodeStart: function(suite, next) {
+          currentlyExecutingSuites.push(suite);
+          defaultResourcesForRunnable(suite.id, suite.parentSuite.id);
+          reporter.suiteStarted(suite.result, next);
+          suite.startTimer();
+        },
+        nodeComplete: function(suite, result, next) {
+          if (suite !== currentSuite()) {
+            throw new Error('Tried to complete the wrong suite');
+          }
+
+          clearResourcesForRunnable(suite.id);
+          currentlyExecutingSuites.pop();
+
+          if (result.status === 'failed') {
+            hasFailures = true;
+          }
+          suite.endTimer();
+          reporter.suiteDone(result, next);
+        },
+        orderChildren: function(node) {
+          return order.sort(node.children);
+        },
+        excludeNode: function(spec) {
+          return !config.specFilter(spec);
+        }
+      });
+
+      if (!processor.processTree().valid) {
+        throw new Error(
+          'Invalid order: would cause a beforeAll or afterAll to be run multiple times'
+        );
+      }
+
+      var jasmineTimer = new j$.Timer();
+      jasmineTimer.start();
+
+      var Promise = customPromise || global.Promise;
+
+      if (Promise) {
+        return new Promise(function(resolve) {
+          runAll(function() {
+            if (onComplete) {
+              onComplete();
+            }
+
+            resolve();
+          });
+        });
+      } else {
+        runAll(function() {
+          if (onComplete) {
+            onComplete();
+          }
+        });
+      }
+
+      function runAll(done) {
+        /**
+         * Information passed to the {@link Reporter#jasmineStarted} event.
+         * @typedef JasmineStartedInfo
+         * @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
+         * @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
+         * @since 2.0.0
+         */
+        reporter.jasmineStarted(
+          {
+            totalSpecsDefined: totalSpecsDefined,
+            order: order
+          },
+          function() {
+            currentlyExecutingSuites.push(topSuite);
+
+            processor.execute(function() {
+              clearResourcesForRunnable(topSuite.id);
+              currentlyExecutingSuites.pop();
+              var overallStatus, incompleteReason;
+
+              if (
+                hasFailures ||
+                topSuite.result.failedExpectations.length > 0
+              ) {
+                overallStatus = 'failed';
+              } else if (focusedRunnables.length > 0) {
+                overallStatus = 'incomplete';
+                incompleteReason = 'fit() or fdescribe() was found';
+              } else if (totalSpecsDefined === 0) {
+                overallStatus = 'incomplete';
+                incompleteReason = 'No specs found';
+              } else {
+                overallStatus = 'passed';
+              }
+
+              /**
+               * Information passed to the {@link Reporter#jasmineDone} event.
+               * @typedef JasmineDoneInfo
+               * @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
+               * @property {Int} totalTime - The total time (in ms) that it took to execute the suite
+               * @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
+               * @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
+               * @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
+               * @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
+               * @since 2.4.0
+               */
+              reporter.jasmineDone(
+                {
+                  overallStatus: overallStatus,
+                  totalTime: jasmineTimer.elapsed(),
+                  incompleteReason: incompleteReason,
+                  order: order,
+                  failedExpectations: topSuite.result.failedExpectations,
+                  deprecationWarnings: topSuite.result.deprecationWarnings
+                },
+                done
+              );
+            });
+          }
+        );
+      }
+    };
+
+    /**
+     * Add a custom reporter to the Jasmine environment.
+     * @name Env#addReporter
+     * @since 2.0.0
+     * @function
+     * @param {Reporter} reporterToAdd The reporter to be added.
+     * @see custom_reporter
+     */
+    this.addReporter = function(reporterToAdd) {
+      reporter.addReporter(reporterToAdd);
+    };
+
+    /**
+     * Provide a fallback reporter if no other reporters have been specified.
+     * @name Env#provideFallbackReporter
+     * @since 2.5.0
+     * @function
+     * @param {Reporter} reporterToAdd The reporter
+     * @see custom_reporter
+     */
+    this.provideFallbackReporter = function(reporterToAdd) {
+      reporter.provideFallbackReporter(reporterToAdd);
+    };
+
+    /**
+     * Clear all registered reporters
+     * @name Env#clearReporters
+     * @since 2.5.2
+     * @function
+     */
+    this.clearReporters = function() {
+      reporter.clearReporters();
+    };
+
+    var spyFactory = new j$.SpyFactory(
+      function getCustomStrategies() {
+        var runnable = currentRunnable();
+
+        if (runnable) {
+          return runnableResources[runnable.id].customSpyStrategies;
+        }
+
+        return {};
+      },
+      function getDefaultStrategyFn() {
+        var runnable = currentRunnable();
+
+        if (runnable) {
+          return runnableResources[runnable.id].defaultStrategyFn;
+        }
+
+        return undefined;
+      },
+      function getPromise() {
+        return customPromise || global.Promise;
+      }
+    );
+
+    var spyRegistry = new j$.SpyRegistry({
+      currentSpies: function() {
+        if (!currentRunnable()) {
+          throw new Error(
+            'Spies must be created in a before function or a spec'
+          );
+        }
+        return runnableResources[currentRunnable().id].spies;
+      },
+      createSpy: function(name, originalFn) {
+        return self.createSpy(name, originalFn);
+      }
+    });
+
+    /**
+     * Configures whether Jasmine should allow the same function to be spied on
+     * more than once during the execution of a spec. By default, spying on
+     * a function that is already a spy will cause an error.
+     * @name Env#allowRespy
+     * @function
+     * @since 2.5.0
+     * @param {boolean} allow Whether to allow respying
+     */
+    this.allowRespy = function(allow) {
+      spyRegistry.allowRespy(allow);
+    };
+
+    this.spyOn = function() {
+      return spyRegistry.spyOn.apply(spyRegistry, arguments);
+    };
+
+    this.spyOnProperty = function() {
+      return spyRegistry.spyOnProperty.apply(spyRegistry, arguments);
+    };
+
+    this.spyOnAllFunctions = function() {
+      return spyRegistry.spyOnAllFunctions.apply(spyRegistry, arguments);
+    };
+
+    this.createSpy = function(name, originalFn) {
+      if (arguments.length === 1 && j$.isFunction_(name)) {
+        originalFn = name;
+        name = originalFn.name;
+      }
+
+      return spyFactory.createSpy(name, originalFn);
+    };
+
+    this.createSpyObj = function(baseName, methodNames, propertyNames) {
+      return spyFactory.createSpyObj(baseName, methodNames, propertyNames);
+    };
+
+    var ensureIsFunction = function(fn, caller) {
+      if (!j$.isFunction_(fn)) {
+        throw new Error(
+          caller + ' expects a function argument; received ' + j$.getType_(fn)
+        );
+      }
+    };
+
+    var ensureIsFunctionOrAsync = function(fn, caller) {
+      if (!j$.isFunction_(fn) && !j$.isAsyncFunction_(fn)) {
+        throw new Error(
+          caller + ' expects a function argument; received ' + j$.getType_(fn)
+        );
+      }
+    };
+
+    function ensureIsNotNested(method) {
+      var runnable = currentRunnable();
+      if (runnable !== null && runnable !== undefined) {
+        throw new Error(
+          "'" + method + "' should only be used in 'describe' function"
+        );
+      }
+    }
+
+    var suiteFactory = function(description) {
+      var suite = new j$.Suite({
+        env: self,
+        id: getNextSuiteId(),
+        description: description,
+        parentSuite: currentDeclarationSuite,
+        timer: new j$.Timer(),
+        expectationFactory: expectationFactory,
+        asyncExpectationFactory: suiteAsyncExpectationFactory,
+        expectationResultFactory: expectationResultFactory,
+        throwOnExpectationFailure: config.oneFailurePerSpec,
+        autoCleanClosures: config.autoCleanClosures
+      });
+
+      return suite;
+    };
+
+    this.describe = function(description, specDefinitions) {
+      ensureIsNotNested('describe');
+      ensureIsFunction(specDefinitions, 'describe');
+      var suite = suiteFactory(description);
+      if (specDefinitions.length > 0) {
+        throw new Error('describe does not expect any arguments');
+      }
+      if (currentDeclarationSuite.markedExcluding) {
+        suite.exclude();
+      }
+      addSpecsToSuite(suite, specDefinitions);
+      if (suite.parentSuite && !suite.children.length) {
+        this.deprecated(
+          'describe with no children (describe() or it()) is ' +
+            'deprecated and will be removed in a future version of Jasmine. ' +
+            'Please either remove the describe or add children to it.'
+        );
+      }
+      return j$.deprecatingSuiteProxy(suite, suite.parentSuite, this);
+    };
+
+    this.xdescribe = function(description, specDefinitions) {
+      ensureIsNotNested('xdescribe');
+      ensureIsFunction(specDefinitions, 'xdescribe');
+      var suite = suiteFactory(description);
+      suite.exclude();
+      addSpecsToSuite(suite, specDefinitions);
+      return j$.deprecatingSuiteProxy(suite, suite.parentSuite, this);
+    };
+
+    var focusedRunnables = [];
+
+    this.fdescribe = function(description, specDefinitions) {
+      ensureIsNotNested('fdescribe');
+      ensureIsFunction(specDefinitions, 'fdescribe');
+      var suite = suiteFactory(description);
+      suite.isFocused = true;
+
+      focusedRunnables.push(suite.id);
+      unfocusAncestor();
+      addSpecsToSuite(suite, specDefinitions);
+
+      return j$.deprecatingSuiteProxy(suite, suite.parentSuite, this);
+    };
+
+    function addSpecsToSuite(suite, specDefinitions) {
+      var parentSuite = currentDeclarationSuite;
+      parentSuite.addChild(suite);
+      currentDeclarationSuite = suite;
+
+      var declarationError = null;
+      try {
+        specDefinitions.call(j$.deprecatingThisProxy(suite, self));
+      } catch (e) {
+        declarationError = e;
+      }
+
+      if (declarationError) {
+        suite.onException(declarationError);
+      }
+
+      currentDeclarationSuite = parentSuite;
+    }
+
+    function findFocusedAncestor(suite) {
+      while (suite) {
+        if (suite.isFocused) {
+          return suite.id;
+        }
+        suite = suite.parentSuite;
+      }
+
+      return null;
+    }
+
+    function unfocusAncestor() {
+      var focusedAncestor = findFocusedAncestor(currentDeclarationSuite);
+      if (focusedAncestor) {
+        for (var i = 0; i < focusedRunnables.length; i++) {
+          if (focusedRunnables[i] === focusedAncestor) {
+            focusedRunnables.splice(i, 1);
+            break;
+          }
+        }
+      }
+    }
+
+    var specFactory = function(description, fn, suite, timeout) {
+      totalSpecsDefined++;
+      var spec = new j$.Spec({
+        id: getNextSpecId(),
+        beforeAndAfterFns: beforeAndAfterFns(suite),
+        expectationFactory: expectationFactory,
+        asyncExpectationFactory: specAsyncExpectationFactory,
+        deprecated: self.deprecated,
+        resultCallback: specResultCallback,
+        getSpecName: function(spec) {
+          return getSpecName(spec, suite);
+        },
+        onStart: specStarted,
+        description: description,
+        expectationResultFactory: expectationResultFactory,
+        queueRunnerFactory: queueRunnerFactory,
+        userContext: function() {
+          return suite.clonedSharedUserContext();
+        },
+        queueableFn: {
+          fn: fn,
+          timeout: timeout || 0
+        },
+        throwOnExpectationFailure: config.oneFailurePerSpec,
+        autoCleanClosures: config.autoCleanClosures,
+        timer: new j$.Timer()
+      });
+      return spec;
+
+      function specResultCallback(result, next) {
+        clearResourcesForRunnable(spec.id);
+        currentSpec = null;
+
+        if (result.status === 'failed') {
+          hasFailures = true;
+        }
+
+        reporter.specDone(result, next);
+      }
+
+      function specStarted(spec, next) {
+        currentSpec = spec;
+        defaultResourcesForRunnable(spec.id, suite.id);
+        reporter.specStarted(spec.result, next);
+      }
+    };
+
+    this.it_ = function(description, fn, timeout) {
+      ensureIsNotNested('it');
+      // it() sometimes doesn't have a fn argument, so only check the type if
+      // it's given.
+      if (arguments.length > 1 && typeof fn !== 'undefined') {
+        ensureIsFunctionOrAsync(fn, 'it');
+      }
+
+      if (timeout) {
+        j$.util.validateTimeout(timeout);
+      }
+
+      var spec = specFactory(description, fn, currentDeclarationSuite, timeout);
+      if (currentDeclarationSuite.markedExcluding) {
+        spec.exclude();
+      }
+      currentDeclarationSuite.addChild(spec);
+
+      return spec;
+    };
+
+    this.it = function(description, fn, timeout) {
+      var spec = this.it_(description, fn, timeout);
+      return j$.deprecatingSpecProxy(spec, this);
+    };
+
+    this.xit = function(description, fn, timeout) {
+      ensureIsNotNested('xit');
+      // xit(), like it(), doesn't always have a fn argument, so only check the
+      // type when needed.
+      if (arguments.length > 1 && typeof fn !== 'undefined') {
+        ensureIsFunctionOrAsync(fn, 'xit');
+      }
+      var spec = this.it_.apply(this, arguments);
+      spec.exclude('Temporarily disabled with xit');
+      return j$.deprecatingSpecProxy(spec, this);
+    };
+
+    this.fit = function(description, fn, timeout) {
+      ensureIsNotNested('fit');
+      ensureIsFunctionOrAsync(fn, 'fit');
+
+      if (timeout) {
+        j$.util.validateTimeout(timeout);
+      }
+      var spec = specFactory(description, fn, currentDeclarationSuite, timeout);
+      currentDeclarationSuite.addChild(spec);
+      focusedRunnables.push(spec.id);
+      unfocusAncestor();
+      return j$.deprecatingSpecProxy(spec, this);
+    };
+
+    /**
+     * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult}
+     * @name Env#setSpecProperty
+     * @since 3.6.0
+     * @function
+     * @param {String} key The name of the property
+     * @param {*} value The value of the property
+     */
+    this.setSpecProperty = function(key, value) {
+      if (!currentRunnable() || currentRunnable() == currentSuite()) {
+        throw new Error(
+          "'setSpecProperty' was used when there was no current spec"
+        );
+      }
+      currentRunnable().setSpecProperty(key, value);
+    };
+
+    /**
+     * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SuiteResult}
+     * @name Env#setSuiteProperty
+     * @since 3.6.0
+     * @function
+     * @param {String} key The name of the property
+     * @param {*} value The value of the property
+     */
+    this.setSuiteProperty = function(key, value) {
+      if (!currentSuite()) {
+        throw new Error(
+          "'setSuiteProperty' was used when there was no current suite"
+        );
+      }
+      currentSuite().setSuiteProperty(key, value);
+    };
+
+    this.expect = function(actual) {
+      if (!currentRunnable()) {
+        throw new Error(
+          "'expect' was used when there was no current spec, this could be because an asynchronous test timed out"
+        );
+      }
+
+      return currentRunnable().expect(actual);
+    };
+
+    this.expectAsync = function(actual) {
+      if (!currentRunnable()) {
+        throw new Error(
+          "'expectAsync' was used when there was no current spec, this could be because an asynchronous test timed out"
+        );
+      }
+
+      return currentRunnable().expectAsync(actual);
+    };
+
+    this.beforeEach = function(beforeEachFunction, timeout) {
+      ensureIsNotNested('beforeEach');
+      ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach');
+
+      if (timeout) {
+        j$.util.validateTimeout(timeout);
+      }
+
+      currentDeclarationSuite.beforeEach({
+        fn: beforeEachFunction,
+        timeout: timeout || 0
+      });
+    };
+
+    this.beforeAll = function(beforeAllFunction, timeout) {
+      ensureIsNotNested('beforeAll');
+      ensureIsFunctionOrAsync(beforeAllFunction, 'beforeAll');
+
+      if (timeout) {
+        j$.util.validateTimeout(timeout);
+      }
+
+      currentDeclarationSuite.beforeAll({
+        fn: beforeAllFunction,
+        timeout: timeout || 0
+      });
+    };
+
+    this.afterEach = function(afterEachFunction, timeout) {
+      ensureIsNotNested('afterEach');
+      ensureIsFunctionOrAsync(afterEachFunction, 'afterEach');
+
+      if (timeout) {
+        j$.util.validateTimeout(timeout);
+      }
+
+      afterEachFunction.isCleanup = true;
+      currentDeclarationSuite.afterEach({
+        fn: afterEachFunction,
+        timeout: timeout || 0
+      });
+    };
+
+    this.afterAll = function(afterAllFunction, timeout) {
+      ensureIsNotNested('afterAll');
+      ensureIsFunctionOrAsync(afterAllFunction, 'afterAll');
+
+      if (timeout) {
+        j$.util.validateTimeout(timeout);
+      }
+
+      currentDeclarationSuite.afterAll({
+        fn: afterAllFunction,
+        timeout: timeout || 0
+      });
+    };
+
+    this.pending = function(message) {
+      var fullMessage = j$.Spec.pendingSpecExceptionMessage;
+      if (message) {
+        fullMessage += message;
+      }
+      throw fullMessage;
+    };
+
+    this.fail = function(error) {
+      if (!currentRunnable()) {
+        throw new Error(
+          "'fail' was used when there was no current spec, this could be because an asynchronous test timed out"
+        );
+      }
+
+      var message = 'Failed';
+      if (error) {
+        message += ': ';
+        if (error.message) {
+          message += error.message;
+        } else if (j$.isString_(error)) {
+          message += error;
+        } else {
+          // pretty print all kind of objects. This includes arrays.
+          message += makePrettyPrinter()(error);
+        }
+      }
+
+      currentRunnable().addExpectationResult(false, {
+        matcherName: '',
+        passed: false,
+        expected: '',
+        actual: '',
+        message: message,
+        error: error && error.message ? error : null
+      });
+
+      if (config.oneFailurePerSpec) {
+        throw new Error(message);
+      }
+    };
+
+    this.cleanup_ = function() {
+      if (globalErrors) {
+        globalErrors.uninstall();
+      }
+    };
+  }
+
+  return Env;
+};
+
+getJasmineRequireObj().JsApiReporter = function(j$) {
+  /**
+   * @name jsApiReporter
+   * @classdesc {@link Reporter} added by default in `boot.js` to record results for retrieval in javascript code. An instance is made available as `jsApiReporter` on the global object.
+   * @class
+   * @hideconstructor
+   */
+  function JsApiReporter(options) {
+    var timer = options.timer || new j$.Timer(),
+      status = 'loaded';
+
+    this.started = false;
+    this.finished = false;
+    this.runDetails = {};
+
+    this.jasmineStarted = function() {
+      this.started = true;
+      status = 'started';
+      timer.start();
+    };
+
+    var executionTime;
+
+    this.jasmineDone = function(runDetails) {
+      this.finished = true;
+      this.runDetails = runDetails;
+      executionTime = timer.elapsed();
+      status = 'done';
+    };
+
+    /**
+     * Get the current status for the Jasmine environment.
+     * @name jsApiReporter#status
+     * @since 2.0.0
+     * @function
+     * @return {String} - One of `loaded`, `started`, or `done`
+     */
+    this.status = function() {
+      return status;
+    };
+
+    var suites = [],
+      suites_hash = {};
+
+    this.suiteStarted = function(result) {
+      suites_hash[result.id] = result;
+    };
+
+    this.suiteDone = function(result) {
+      storeSuite(result);
+    };
+
+    /**
+     * Get the results for a set of suites.
+     *
+     * Retrievable in slices for easier serialization.
+     * @name jsApiReporter#suiteResults
+     * @since 2.1.0
+     * @function
+     * @param {Number} index - The position in the suites list to start from.
+     * @param {Number} length - Maximum number of suite results to return.
+     * @return {SuiteResult[]}
+     */
+    this.suiteResults = function(index, length) {
+      return suites.slice(index, index + length);
+    };
+
+    function storeSuite(result) {
+      suites.push(result);
+      suites_hash[result.id] = result;
+    }
+
+    /**
+     * Get all of the suites in a single object, with their `id` as the key.
+     * @name jsApiReporter#suites
+     * @since 2.0.0
+     * @function
+     * @return {Object} - Map of suite id to {@link SuiteResult}
+     */
+    this.suites = function() {
+      return suites_hash;
+    };
+
+    var specs = [];
+
+    this.specDone = function(result) {
+      specs.push(result);
+    };
+
+    /**
+     * Get the results for a set of specs.
+     *
+     * Retrievable in slices for easier serialization.
+     * @name jsApiReporter#specResults
+     * @since 2.0.0
+     * @function
+     * @param {Number} index - The position in the specs list to start from.
+     * @param {Number} length - Maximum number of specs results to return.
+     * @return {SpecResult[]}
+     */
+    this.specResults = function(index, length) {
+      return specs.slice(index, index + length);
+    };
+
+    /**
+     * Get all spec results.
+     * @name jsApiReporter#specs
+     * @since 2.0.0
+     * @function
+     * @return {SpecResult[]}
+     */
+    this.specs = function() {
+      return specs;
+    };
+
+    /**
+     * Get the number of milliseconds it took for the full Jasmine suite to run.
+     * @name jsApiReporter#executionTime
+     * @since 2.0.0
+     * @function
+     * @return {Number}
+     */
+    this.executionTime = function() {
+      return executionTime;
+    };
+  }
+
+  return JsApiReporter;
+};
+
+getJasmineRequireObj().Any = function(j$) {
+  function Any(expectedObject) {
+    if (typeof expectedObject === 'undefined') {
+      throw new TypeError(
+        'jasmine.any() expects to be passed a constructor function. ' +
+          'Please pass one or use jasmine.anything() to match any object.'
+      );
+    }
+    this.expectedObject = expectedObject;
+  }
+
+  Any.prototype.asymmetricMatch = function(other) {
+    if (this.expectedObject == String) {
+      return typeof other == 'string' || other instanceof String;
+    }
+
+    if (this.expectedObject == Number) {
+      return typeof other == 'number' || other instanceof Number;
+    }
+
+    if (this.expectedObject == Function) {
+      return typeof other == 'function' || other instanceof Function;
+    }
+
+    if (this.expectedObject == Object) {
+      return other !== null && typeof other == 'object';
+    }
+
+    if (this.expectedObject == Boolean) {
+      return typeof other == 'boolean';
+    }
+
+    /* jshint -W122 */
+    /* global Symbol */
+    if (typeof Symbol != 'undefined' && this.expectedObject == Symbol) {
+      return typeof other == 'symbol';
+    }
+    /* jshint +W122 */
+
+    return other instanceof this.expectedObject;
+  };
+
+  Any.prototype.jasmineToString = function() {
+    return '<jasmine.any(' + j$.fnNameFor(this.expectedObject) + ')>';
+  };
+
+  return Any;
+};
+
+getJasmineRequireObj().Anything = function(j$) {
+  function Anything() {}
+
+  Anything.prototype.asymmetricMatch = function(other) {
+    return !j$.util.isUndefined(other) && other !== null;
+  };
+
+  Anything.prototype.jasmineToString = function() {
+    return '<jasmine.anything>';
+  };
+
+  return Anything;
+};
+
+getJasmineRequireObj().ArrayContaining = function(j$) {
+  function ArrayContaining(sample) {
+    this.sample = sample;
+  }
+
+  ArrayContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
+    if (!j$.isArray_(this.sample)) {
+      throw new Error(
+        'You must provide an array to arrayContaining, not ' +
+          j$.basicPrettyPrinter_(this.sample) +
+          '.'
+      );
+    }
+
+    // If the actual parameter is not an array, we can fail immediately, since it couldn't
+    // possibly be an "array containing" anything. However, we also want an empty sample
+    // array to match anything, so we need to double-check we aren't in that case
+    if (!j$.isArray_(other) && this.sample.length > 0) {
+      return false;
+    }
+
+    for (var i = 0; i < this.sample.length; i++) {
+      var item = this.sample[i];
+      if (!matchersUtil.contains(other, item)) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ArrayContaining.prototype.jasmineToString = function(pp) {
+    return '<jasmine.arrayContaining(' + pp(this.sample) + ')>';
+  };
+
+  return ArrayContaining;
+};
+
+getJasmineRequireObj().ArrayWithExactContents = function(j$) {
+  function ArrayWithExactContents(sample) {
+    this.sample = sample;
+  }
+
+  ArrayWithExactContents.prototype.asymmetricMatch = function(
+    other,
+    matchersUtil
+  ) {
+    if (!j$.isArray_(this.sample)) {
+      throw new Error(
+        'You must provide an array to arrayWithExactContents, not ' +
+          j$.basicPrettyPrinter_(this.sample) +
+          '.'
+      );
+    }
+
+    if (this.sample.length !== other.length) {
+      return false;
+    }
+
+    for (var i = 0; i < this.sample.length; i++) {
+      var item = this.sample[i];
+      if (!matchersUtil.contains(other, item)) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ArrayWithExactContents.prototype.jasmineToString = function(pp) {
+    return '<jasmine.arrayWithExactContents(' + pp(this.sample) + ')>';
+  };
+
+  return ArrayWithExactContents;
+};
+
+getJasmineRequireObj().Empty = function(j$) {
+  function Empty() {}
+
+  Empty.prototype.asymmetricMatch = function(other) {
+    if (j$.isString_(other) || j$.isArray_(other) || j$.isTypedArray_(other)) {
+      return other.length === 0;
+    }
+
+    if (j$.isMap(other) || j$.isSet(other)) {
+      return other.size === 0;
+    }
+
+    if (j$.isObject_(other)) {
+      return Object.keys(other).length === 0;
+    }
+    return false;
+  };
+
+  Empty.prototype.jasmineToString = function() {
+    return '<jasmine.empty>';
+  };
+
+  return Empty;
+};
+
+getJasmineRequireObj().Falsy = function(j$) {
+  function Falsy() {}
+
+  Falsy.prototype.asymmetricMatch = function(other) {
+    return !other;
+  };
+
+  Falsy.prototype.jasmineToString = function() {
+    return '<jasmine.falsy>';
+  };
+
+  return Falsy;
+};
+
+getJasmineRequireObj().MapContaining = function(j$) {
+  function MapContaining(sample) {
+    if (!j$.isMap(sample)) {
+      throw new Error(
+        'You must provide a map to `mapContaining`, not ' +
+          j$.basicPrettyPrinter_(sample)
+      );
+    }
+
+    this.sample = sample;
+  }
+
+  MapContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
+    if (!j$.isMap(other)) return false;
+
+    var hasAllMatches = true;
+    j$.util.forEachBreakable(this.sample, function(breakLoop, value, key) {
+      // for each key/value pair in `sample`
+      // there should be at least one pair in `other` whose key and value both match
+      var hasMatch = false;
+      j$.util.forEachBreakable(other, function(oBreakLoop, oValue, oKey) {
+        if (
+          matchersUtil.equals(oKey, key) &&
+          matchersUtil.equals(oValue, value)
+        ) {
+          hasMatch = true;
+          oBreakLoop();
+        }
+      });
+      if (!hasMatch) {
+        hasAllMatches = false;
+        breakLoop();
+      }
+    });
+
+    return hasAllMatches;
+  };
+
+  MapContaining.prototype.jasmineToString = function(pp) {
+    return '<jasmine.mapContaining(' + pp(this.sample) + ')>';
+  };
+
+  return MapContaining;
+};
+
+getJasmineRequireObj().NotEmpty = function(j$) {
+  function NotEmpty() {}
+
+  NotEmpty.prototype.asymmetricMatch = function(other) {
+    if (j$.isString_(other) || j$.isArray_(other) || j$.isTypedArray_(other)) {
+      return other.length !== 0;
+    }
+
+    if (j$.isMap(other) || j$.isSet(other)) {
+      return other.size !== 0;
+    }
+
+    if (j$.isObject_(other)) {
+      return Object.keys(other).length !== 0;
+    }
+
+    return false;
+  };
+
+  NotEmpty.prototype.jasmineToString = function() {
+    return '<jasmine.notEmpty>';
+  };
+
+  return NotEmpty;
+};
+
+getJasmineRequireObj().ObjectContaining = function(j$) {
+  function ObjectContaining(sample) {
+    this.sample = sample;
+  }
+
+  function getPrototype(obj) {
+    if (Object.getPrototypeOf) {
+      return Object.getPrototypeOf(obj);
+    }
+
+    if (obj.constructor.prototype == obj) {
+      return null;
+    }
+
+    return obj.constructor.prototype;
+  }
+
+  function hasProperty(obj, property) {
+    if (!obj || typeof obj !== 'object') {
+      return false;
+    }
+
+    if (Object.prototype.hasOwnProperty.call(obj, property)) {
+      return true;
+    }
+
+    return hasProperty(getPrototype(obj), property);
+  }
+
+  ObjectContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
+    if (typeof this.sample !== 'object') {
+      throw new Error(
+        "You must provide an object to objectContaining, not '" +
+          this.sample +
+          "'."
+      );
+    }
+    if (typeof other !== 'object') {
+      return false;
+    }
+
+    for (var property in this.sample) {
+      if (
+        !hasProperty(other, property) ||
+        !matchersUtil.equals(this.sample[property], other[property])
+      ) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ObjectContaining.prototype.valuesForDiff_ = function(other, pp) {
+    if (!j$.isObject_(other)) {
+      return {
+        self: this.jasmineToString(pp),
+        other: other
+      };
+    }
+
+    var filteredOther = {};
+    Object.keys(this.sample).forEach(function(k) {
+      // eq short-circuits comparison of objects that have different key sets,
+      // so include all keys even if undefined.
+      filteredOther[k] = other[k];
+    });
+
+    return {
+      self: this.sample,
+      other: filteredOther
+    };
+  };
+
+  ObjectContaining.prototype.jasmineToString = function(pp) {
+    return '<jasmine.objectContaining(' + pp(this.sample) + ')>';
+  };
+
+  return ObjectContaining;
+};
+
+getJasmineRequireObj().SetContaining = function(j$) {
+  function SetContaining(sample) {
+    if (!j$.isSet(sample)) {
+      throw new Error(
+        'You must provide a set to `setContaining`, not ' +
+          j$.basicPrettyPrinter_(sample)
+      );
+    }
+
+    this.sample = sample;
+  }
+
+  SetContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
+    if (!j$.isSet(other)) return false;
+
+    var hasAllMatches = true;
+    j$.util.forEachBreakable(this.sample, function(breakLoop, item) {
+      // for each item in `sample` there should be at least one matching item in `other`
+      // (not using `matchersUtil.contains` because it compares set members by reference,
+      // not by deep value equality)
+      var hasMatch = false;
+      j$.util.forEachBreakable(other, function(oBreakLoop, oItem) {
+        if (matchersUtil.equals(oItem, item)) {
+          hasMatch = true;
+          oBreakLoop();
+        }
+      });
+      if (!hasMatch) {
+        hasAllMatches = false;
+        breakLoop();
+      }
+    });
+
+    return hasAllMatches;
+  };
+
+  SetContaining.prototype.jasmineToString = function(pp) {
+    return '<jasmine.setContaining(' + pp(this.sample) + ')>';
+  };
+
+  return SetContaining;
+};
+
+getJasmineRequireObj().StringContaining = function(j$) {
+  function StringContaining(expected) {
+    if (!j$.isString_(expected)) {
+      throw new Error('Expected is not a String');
+    }
+
+    this.expected = expected;
+  }
+
+  StringContaining.prototype.asymmetricMatch = function(other) {
+    if (!j$.isString_(other)) {
+      // Arrays, etc. don't match no matter what their indexOf returns.
+      return false;
+    }
+
+    return other.indexOf(this.expected) !== -1;
+  };
+
+  StringContaining.prototype.jasmineToString = function() {
+    return '<jasmine.stringContaining("' + this.expected + '")>';
+  };
+
+  return StringContaining;
+};
+
+getJasmineRequireObj().StringMatching = function(j$) {
+  function StringMatching(expected) {
+    if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) {
+      throw new Error('Expected is not a String or a RegExp');
+    }
+
+    this.regexp = new RegExp(expected);
+  }
+
+  StringMatching.prototype.asymmetricMatch = function(other) {
+    return this.regexp.test(other);
+  };
+
+  StringMatching.prototype.jasmineToString = function() {
+    return '<jasmine.stringMatching(' + this.regexp + ')>';
+  };
+
+  return StringMatching;
+};
+
+getJasmineRequireObj().Truthy = function(j$) {
+  function Truthy() {}
+
+  Truthy.prototype.asymmetricMatch = function(other) {
+    return !!other;
+  };
+
+  Truthy.prototype.jasmineToString = function() {
+    return '<jasmine.truthy>';
+  };
+
+  return Truthy;
+};
+
+getJasmineRequireObj().asymmetricEqualityTesterArgCompatShim = function(j$) {
+  /*
+    Older versions of Jasmine passed an array of custom equality testers as the
+    second argument to each asymmetric equality tester's `asymmetricMatch`
+    method. Newer versions will pass a `MatchersUtil` instance. The
+    asymmetricEqualityTesterArgCompatShim allows for a graceful migration from
+    the old interface to the new by "being" both an array of custom equality
+    testers and a `MatchersUtil` at the same time.
+
+    This code should be removed in the next major release.
+   */
+
+  var likelyArrayProps = [
+    'concat',
+    'constructor',
+    'copyWithin',
+    'entries',
+    'every',
+    'fill',
+    'filter',
+    'find',
+    'findIndex',
+    'flat',
+    'flatMap',
+    'forEach',
+    'includes',
+    'indexOf',
+    'join',
+    'keys',
+    'lastIndexOf',
+    'length',
+    'map',
+    'pop',
+    'push',
+    'reduce',
+    'reduceRight',
+    'reverse',
+    'shift',
+    'slice',
+    'some',
+    'sort',
+    'splice',
+    'toLocaleString',
+    'toSource',
+    'toString',
+    'unshift',
+    'values'
+  ];
+
+  function asymmetricEqualityTesterArgCompatShim(
+    matchersUtil,
+    customEqualityTesters
+  ) {
+    var self = Object.create(matchersUtil);
+
+    copyAndDeprecate(self, customEqualityTesters, 'length');
+
+    for (i = 0; i < customEqualityTesters.length; i++) {
+      copyAndDeprecate(self, customEqualityTesters, i);
+    }
+
+    // Avoid copying array props if we've previously done so,
+    // to avoid triggering our own deprecation warnings.
+    if (!self.isAsymmetricEqualityTesterArgCompatShim_) {
+      copyAndDeprecateArrayMethods(self);
+    }
+
+    self.isAsymmetricEqualityTesterArgCompatShim_ = true;
+    return self;
+  }
+
+  function copyAndDeprecateArrayMethods(dest) {
+    var props = arrayProps(),
+      i,
+      k;
+
+    for (i = 0; i < props.length; i++) {
+      k = props[i];
+
+      // Skip length (dealt with above), and anything that collides with
+      // MatchesUtil e.g. an Array.prototype.contains method added by user code
+      if (k !== 'length' && !dest[k]) {
+        copyAndDeprecate(dest, Array.prototype, k);
+      }
+    }
+  }
+
+  function copyAndDeprecate(dest, src, propName) {
+    Object.defineProperty(dest, propName, {
+      get: function() {
+        j$.getEnv().deprecated(
+          'The second argument to asymmetricMatch is now a ' +
+            'MatchersUtil. Using it as an array of custom equality testers is ' +
+            'deprecated and will stop working in a future release. ' +
+            'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#asymmetricMatch-cet> for details.'
+        );
+        return src[propName];
+      }
+    });
+  }
+
+  function arrayProps() {
+    var props, a, k;
+
+    if (!Object.getOwnPropertyDescriptors) {
+      return likelyArrayProps.filter(function(k) {
+        return Array.prototype.hasOwnProperty(k);
+      });
+    }
+
+    props = Object.getOwnPropertyDescriptors(Array.prototype); // eslint-disable-line compat/compat
+    a = [];
+
+    for (k in props) {
+      a.push(k);
+    }
+
+    return a;
+  }
+
+  return asymmetricEqualityTesterArgCompatShim;
+};
+
+getJasmineRequireObj().CallTracker = function(j$) {
+  /**
+   * @namespace Spy#calls
+   * @since 2.0.0
+   */
+  function CallTracker() {
+    var calls = [];
+    var opts = {};
+
+    this.track = function(context) {
+      if (opts.cloneArgs) {
+        context.args = j$.util.cloneArgs(context.args);
+      }
+      calls.push(context);
+    };
+
+    /**
+     * Check whether this spy has been invoked.
+     * @name Spy#calls#any
+     * @since 2.0.0
+     * @function
+     * @return {Boolean}
+     */
+    this.any = function() {
+      return !!calls.length;
+    };
+
+    /**
+     * Get the number of invocations of this spy.
+     * @name Spy#calls#count
+     * @since 2.0.0
+     * @function
+     * @return {Integer}
+     */
+    this.count = function() {
+      return calls.length;
+    };
+
+    /**
+     * Get the arguments that were passed to a specific invocation of this spy.
+     * @name Spy#calls#argsFor
+     * @since 2.0.0
+     * @function
+     * @param {Integer} index The 0-based invocation index.
+     * @return {Array}
+     */
+    this.argsFor = function(index) {
+      var call = calls[index];
+      return call ? call.args : [];
+    };
+
+    /**
+     * Get the "this" object that was passed to a specific invocation of this spy.
+     * @name Spy#calls#thisFor
+     * @since 3.8.0
+     * @function
+     * @param {Integer} index The 0-based invocation index.
+     * @return {Object?}
+     */
+    this.thisFor = function(index) {
+      var call = calls[index];
+      return call ? call.object : undefined;
+    };
+
+    /**
+     * Get the raw calls array for this spy.
+     * @name Spy#calls#all
+     * @since 2.0.0
+     * @function
+     * @return {Spy.callData[]}
+     */
+    this.all = function() {
+      return calls;
+    };
+
+    /**
+     * Get all of the arguments for each invocation of this spy in the order they were received.
+     * @name Spy#calls#allArgs
+     * @since 2.0.0
+     * @function
+     * @return {Array}
+     */
+    this.allArgs = function() {
+      var callArgs = [];
+      for (var i = 0; i < calls.length; i++) {
+        callArgs.push(calls[i].args);
+      }
+
+      return callArgs;
+    };
+
+    /**
+     * Get the first invocation of this spy.
+     * @name Spy#calls#first
+     * @since 2.0.0
+     * @function
+     * @return {ObjecSpy.callData}
+     */
+    this.first = function() {
+      return calls[0];
+    };
+
+    /**
+     * Get the most recent invocation of this spy.
+     * @name Spy#calls#mostRecent
+     * @since 2.0.0
+     * @function
+     * @return {ObjecSpy.callData}
+     */
+    this.mostRecent = function() {
+      return calls[calls.length - 1];
+    };
+
+    /**
+     * Reset this spy as if it has never been called.
+     * @name Spy#calls#reset
+     * @since 2.0.0
+     * @function
+     */
+    this.reset = function() {
+      calls = [];
+    };
+
+    /**
+     * Set this spy to do a shallow clone of arguments passed to each invocation.
+     * @name Spy#calls#saveArgumentsByValue
+     * @since 2.5.0
+     * @function
+     */
+    this.saveArgumentsByValue = function() {
+      opts.cloneArgs = true;
+    };
+  }
+
+  return CallTracker;
+};
+
+getJasmineRequireObj().clearStack = function(j$) {
+  var maxInlineCallCount = 10;
+
+  function messageChannelImpl(global, setTimeout) {
+    var channel = new global.MessageChannel(),
+      head = {},
+      tail = head;
+
+    var taskRunning = false;
+    channel.port1.onmessage = function() {
+      head = head.next;
+      var task = head.task;
+      delete head.task;
+
+      if (taskRunning) {
+        global.setTimeout(task, 0);
+      } else {
+        try {
+          taskRunning = true;
+          task();
+        } finally {
+          taskRunning = false;
+        }
+      }
+    };
+
+    var currentCallCount = 0;
+    return function clearStack(fn) {
+      currentCallCount++;
+
+      if (currentCallCount < maxInlineCallCount) {
+        tail = tail.next = { task: fn };
+        channel.port2.postMessage(0);
+      } else {
+        currentCallCount = 0;
+        setTimeout(fn);
+      }
+    };
+  }
+
+  function getClearStack(global) {
+    var currentCallCount = 0;
+    var realSetTimeout = global.setTimeout;
+    var setTimeoutImpl = function clearStack(fn) {
+      Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]);
+    };
+
+    if (j$.isFunction_(global.setImmediate)) {
+      var realSetImmediate = global.setImmediate;
+      return function(fn) {
+        currentCallCount++;
+
+        if (currentCallCount < maxInlineCallCount) {
+          realSetImmediate(fn);
+        } else {
+          currentCallCount = 0;
+
+          setTimeoutImpl(fn);
+        }
+      };
+    } else if (!j$.util.isUndefined(global.MessageChannel)) {
+      return messageChannelImpl(global, setTimeoutImpl);
+    } else {
+      return setTimeoutImpl;
+    }
+  }
+
+  return getClearStack;
+};
+
+getJasmineRequireObj().Clock = function() {
+  /* global process */
+  var NODE_JS =
+    typeof process !== 'undefined' &&
+    process.versions &&
+    typeof process.versions.node === 'string';
+
+  /**
+   * @class Clock
+   * @since 1.3.0
+   * @classdesc Jasmine's mock clock is used when testing time dependent code.<br>
+   * _Note:_ Do not construct this directly. You can get the current clock with
+   * {@link jasmine.clock}.
+   * @hideconstructor
+   */
+  function Clock(global, delayedFunctionSchedulerFactory, mockDate) {
+    var self = this,
+      realTimingFunctions = {
+        setTimeout: global.setTimeout,
+        clearTimeout: global.clearTimeout,
+        setInterval: global.setInterval,
+        clearInterval: global.clearInterval
+      },
+      fakeTimingFunctions = {
+        setTimeout: setTimeout,
+        clearTimeout: clearTimeout,
+        setInterval: setInterval,
+        clearInterval: clearInterval
+      },
+      installed = false,
+      delayedFunctionScheduler,
+      timer;
+
+    self.FakeTimeout = FakeTimeout;
+
+    /**
+     * Install the mock clock over the built-in methods.
+     * @name Clock#install
+     * @since 2.0.0
+     * @function
+     * @return {Clock}
+     */
+    self.install = function() {
+      if (!originalTimingFunctionsIntact()) {
+        throw new Error(
+          'Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'
+        );
+      }
+      replace(global, fakeTimingFunctions);
+      timer = fakeTimingFunctions;
+      delayedFunctionScheduler = delayedFunctionSchedulerFactory();
+      installed = true;
+
+      return self;
+    };
+
+    /**
+     * Uninstall the mock clock, returning the built-in methods to their places.
+     * @name Clock#uninstall
+     * @since 2.0.0
+     * @function
+     */
+    self.uninstall = function() {
+      delayedFunctionScheduler = null;
+      mockDate.uninstall();
+      replace(global, realTimingFunctions);
+
+      timer = realTimingFunctions;
+      installed = false;
+    };
+
+    /**
+     * Execute a function with a mocked Clock
+     *
+     * The clock will be {@link Clock#install|install}ed before the function is called and {@link Clock#uninstall|uninstall}ed in a `finally` after the function completes.
+     * @name Clock#withMock
+     * @since 2.3.0
+     * @function
+     * @param {Function} closure The function to be called.
+     */
+    self.withMock = function(closure) {
+      this.install();
+      try {
+        closure();
+      } finally {
+        this.uninstall();
+      }
+    };
+
+    /**
+     * Instruct the installed Clock to also mock the date returned by `new Date()`
+     * @name Clock#mockDate
+     * @since 2.1.0
+     * @function
+     * @param {Date} [initialDate=now] The `Date` to provide.
+     */
+    self.mockDate = function(initialDate) {
+      mockDate.install(initialDate);
+    };
+
+    self.setTimeout = function(fn, delay, params) {
+      return Function.prototype.apply.apply(timer.setTimeout, [
+        global,
+        arguments
+      ]);
+    };
+
+    self.setInterval = function(fn, delay, params) {
+      return Function.prototype.apply.apply(timer.setInterval, [
+        global,
+        arguments
+      ]);
+    };
+
+    self.clearTimeout = function(id) {
+      return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
+    };
+
+    self.clearInterval = function(id) {
+      return Function.prototype.call.apply(timer.clearInterval, [global, id]);
+    };
+
+    /**
+     * Tick the Clock forward, running any enqueued timeouts along the way
+     * @name Clock#tick
+     * @since 1.3.0
+     * @function
+     * @param {int} millis The number of milliseconds to tick.
+     */
+    self.tick = function(millis) {
+      if (installed) {
+        delayedFunctionScheduler.tick(millis, function(millis) {
+          mockDate.tick(millis);
+        });
+      } else {
+        throw new Error(
+          'Mock clock is not installed, use jasmine.clock().install()'
+        );
+      }
+    };
+
+    return self;
+
+    function originalTimingFunctionsIntact() {
+      return (
+        global.setTimeout === realTimingFunctions.setTimeout &&
+        global.clearTimeout === realTimingFunctions.clearTimeout &&
+        global.setInterval === realTimingFunctions.setInterval &&
+        global.clearInterval === realTimingFunctions.clearInterval
+      );
+    }
+
+    function replace(dest, source) {
+      for (var prop in source) {
+        dest[prop] = source[prop];
+      }
+    }
+
+    function setTimeout(fn, delay) {
+      if (!NODE_JS) {
+        return delayedFunctionScheduler.scheduleFunction(
+          fn,
+          delay,
+          argSlice(arguments, 2)
+        );
+      }
+
+      var timeout = new FakeTimeout();
+
+      delayedFunctionScheduler.scheduleFunction(
+        fn,
+        delay,
+        argSlice(arguments, 2),
+        false,
+        timeout
+      );
+
+      return timeout;
+    }
+
+    function clearTimeout(id) {
+      return delayedFunctionScheduler.removeFunctionWithId(id);
+    }
+
+    function setInterval(fn, interval) {
+      if (!NODE_JS) {
+        return delayedFunctionScheduler.scheduleFunction(
+          fn,
+          interval,
+          argSlice(arguments, 2),
+          true
+        );
+      }
+
+      var timeout = new FakeTimeout();
+
+      delayedFunctionScheduler.scheduleFunction(
+        fn,
+        interval,
+        argSlice(arguments, 2),
+        true,
+        timeout
+      );
+
+      return timeout;
+    }
+
+    function clearInterval(id) {
+      return delayedFunctionScheduler.removeFunctionWithId(id);
+    }
+
+    function argSlice(argsObj, n) {
+      return Array.prototype.slice.call(argsObj, n);
+    }
+  }
+
+  /**
+   * Mocks Node.js Timeout class
+   */
+  function FakeTimeout() {}
+
+  FakeTimeout.prototype.ref = function() {
+    return this;
+  };
+
+  FakeTimeout.prototype.unref = function() {
+    return this;
+  };
+
+  return Clock;
+};
+
+getJasmineRequireObj().DelayedFunctionScheduler = function(j$) {
+  function DelayedFunctionScheduler() {
+    var self = this;
+    var scheduledLookup = [];
+    var scheduledFunctions = {};
+    var currentTime = 0;
+    var delayedFnCount = 0;
+    var deletedKeys = [];
+    var ticking = false;
+
+    self.tick = function(millis, tickDate) {
+      if (ticking) {
+        j$.getEnv().deprecated(
+          'The behavior of reentrant calls to jasmine.clock().tick() will ' +
+            'change in a future version. Either modify the affected spec to ' +
+            'not call tick() from within a setTimeout or setInterval handler, ' +
+            'or be aware that it may behave differently in the future. See ' +
+            '<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-reentrant-calls-to-jasmine-clock-tick> ' +
+            'for details.'
+        );
+      }
+
+      ticking = true;
+
+      try {
+        millis = millis || 0;
+        var endTime = currentTime + millis;
+
+        runScheduledFunctions(endTime, tickDate);
+        currentTime = endTime;
+      } finally {
+        ticking = false;
+      }
+    };
+
+    self.scheduleFunction = function(
+      funcToCall,
+      millis,
+      params,
+      recurring,
+      timeoutKey,
+      runAtMillis
+    ) {
+      var f;
+      if (typeof funcToCall === 'string') {
+        /* jshint evil: true */
+        f = function() {
+          return eval(funcToCall);
+        };
+        /* jshint evil: false */
+      } else {
+        f = funcToCall;
+      }
+
+      millis = millis || 0;
+      timeoutKey = timeoutKey || ++delayedFnCount;
+      runAtMillis = runAtMillis || currentTime + millis;
+
+      var funcToSchedule = {
+        runAtMillis: runAtMillis,
+        funcToCall: f,
+        recurring: recurring,
+        params: params,
+        timeoutKey: timeoutKey,
+        millis: millis
+      };
+
+      if (runAtMillis in scheduledFunctions) {
+        scheduledFunctions[runAtMillis].push(funcToSchedule);
+      } else {
+        scheduledFunctions[runAtMillis] = [funcToSchedule];
+        scheduledLookup.push(runAtMillis);
+        scheduledLookup.sort(function(a, b) {
+          return a - b;
+        });
+      }
+
+      return timeoutKey;
+    };
+
+    self.removeFunctionWithId = function(timeoutKey) {
+      deletedKeys.push(timeoutKey);
+
+      for (var runAtMillis in scheduledFunctions) {
+        var funcs = scheduledFunctions[runAtMillis];
+        var i = indexOfFirstToPass(funcs, function(func) {
+          return func.timeoutKey === timeoutKey;
+        });
+
+        if (i > -1) {
+          if (funcs.length === 1) {
+            delete scheduledFunctions[runAtMillis];
+            deleteFromLookup(runAtMillis);
+          } else {
+            funcs.splice(i, 1);
+          }
+
+          // intervals get rescheduled when executed, so there's never more
+          // than a single scheduled function with a given timeoutKey
+          break;
+        }
+      }
+    };
+
+    return self;
+
+    function indexOfFirstToPass(array, testFn) {
+      var index = -1;
+
+      for (var i = 0; i < array.length; ++i) {
+        if (testFn(array[i])) {
+          index = i;
+          break;
+        }
+      }
+
+      return index;
+    }
+
+    function deleteFromLookup(key) {
+      var value = Number(key);
+      var i = indexOfFirstToPass(scheduledLookup, function(millis) {
+        return millis === value;
+      });
+
+      if (i > -1) {
+        scheduledLookup.splice(i, 1);
+      }
+    }
+
+    function reschedule(scheduledFn) {
+      self.scheduleFunction(
+        scheduledFn.funcToCall,
+        scheduledFn.millis,
+        scheduledFn.params,
+        true,
+        scheduledFn.timeoutKey,
+        scheduledFn.runAtMillis + scheduledFn.millis
+      );
+    }
+
+    function forEachFunction(funcsToRun, callback) {
+      for (var i = 0; i < funcsToRun.length; ++i) {
+        callback(funcsToRun[i]);
+      }
+    }
+
+    function runScheduledFunctions(endTime, tickDate) {
+      tickDate = tickDate || function() {};
+      if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
+        tickDate(endTime - currentTime);
+        return;
+      }
+
+      do {
+        deletedKeys = [];
+        var newCurrentTime = scheduledLookup.shift();
+        tickDate(newCurrentTime - currentTime);
+
+        currentTime = newCurrentTime;
+
+        var funcsToRun = scheduledFunctions[currentTime];
+
+        delete scheduledFunctions[currentTime];
+
+        forEachFunction(funcsToRun, function(funcToRun) {
+          if (funcToRun.recurring) {
+            reschedule(funcToRun);
+          }
+        });
+
+        forEachFunction(funcsToRun, function(funcToRun) {
+          if (j$.util.arrayContains(deletedKeys, funcToRun.timeoutKey)) {
+            // skip a timeoutKey deleted whilst we were running
+            return;
+          }
+          funcToRun.funcToCall.apply(null, funcToRun.params || []);
+        });
+        deletedKeys = [];
+      } while (
+        scheduledLookup.length > 0 &&
+        // checking first if we're out of time prevents setTimeout(0)
+        // scheduled in a funcToRun from forcing an extra iteration
+        currentTime !== endTime &&
+        scheduledLookup[0] <= endTime
+      );
+
+      // ran out of functions to call, but still time left on the clock
+      if (currentTime !== endTime) {
+        tickDate(endTime - currentTime);
+      }
+    }
+  }
+
+  return DelayedFunctionScheduler;
+};
+
+/* eslint-disable compat/compat */
+// TODO: Remove this in the next major release.
+getJasmineRequireObj().deprecatingSpecProxy = function(j$) {
+  function isMember(target, prop) {
+    return (
+      Object.keys(target).indexOf(prop) !== -1 ||
+      Object.keys(j$.Spec.prototype).indexOf(prop) !== -1
+    );
+  }
+
+  function isAllowedMember(prop) {
+    return prop === 'id' || prop === 'description' || prop === 'getFullName';
+  }
+
+  function msg(member) {
+    var memberName = member.toString().replace(/^Symbol\((.+)\)$/, '$1');
+    return (
+      'Access to private Spec members (in this case `' +
+      memberName +
+      '`) is not supported and will break in ' +
+      'a future release. See <https://jasmine.github.io/api/edge/Spec.html> ' +
+      'for correct usage.'
+    );
+  }
+
+  try {
+    new Proxy({}, {});
+  } catch (e) {
+    // Environment does not support Poxy.
+    return function(spec) {
+      return spec;
+    };
+  }
+
+  function DeprecatingSpecProxyHandler(env) {
+    this._env = env;
+  }
+
+  DeprecatingSpecProxyHandler.prototype.get = function(target, prop, receiver) {
+    this._maybeDeprecate(target, prop);
+
+    if (prop === 'getFullName') {
+      // getFullName calls a private method. Re-bind 'this' to avoid a bogus
+      // deprecation warning.
+      return target.getFullName.bind(target);
+    } else {
+      return target[prop];
+    }
+  };
+
+  DeprecatingSpecProxyHandler.prototype.set = function(target, prop, value) {
+    this._maybeDeprecate(target, prop);
+    return (target[prop] = value);
+  };
+
+  DeprecatingSpecProxyHandler.prototype._maybeDeprecate = function(
+    target,
+    prop
+  ) {
+    if (isMember(target, prop) && !isAllowedMember(prop)) {
+      this._env.deprecated(msg(prop));
+    }
+  };
+
+  function deprecatingSpecProxy(spec, env) {
+    return new Proxy(spec, new DeprecatingSpecProxyHandler(env));
+  }
+
+  return deprecatingSpecProxy;
+};
+
+/* eslint-disable compat/compat */
+// TODO: Remove this in the next major release.
+getJasmineRequireObj().deprecatingSuiteProxy = function(j$) {
+  var allowedMembers = [
+    'id',
+    'children',
+    'description',
+    'parentSuite',
+    'getFullName'
+  ];
+
+  function isMember(target, prop) {
+    return (
+      Object.keys(target).indexOf(prop) !== -1 ||
+      Object.keys(j$.Suite.prototype).indexOf(prop) !== -1
+    );
+  }
+
+  function isAllowedMember(prop) {
+    return allowedMembers.indexOf(prop) !== -1;
+  }
+
+  function msg(member) {
+    var memberName = member.toString().replace(/^Symbol\((.+)\)$/, '$1');
+    return (
+      'Access to private Suite members (in this case `' +
+      memberName +
+      '`) is not supported and will break in ' +
+      'a future release. See <https://jasmine.github.io/api/edge/Suite.html> ' +
+      'for correct usage.'
+    );
+  }
+  try {
+    new Proxy({}, {});
+  } catch (e) {
+    // Environment does not support Poxy.
+    return function(suite) {
+      return suite;
+    };
+  }
+
+  function DeprecatingSuiteProxyHandler(parentSuite, env) {
+    this._parentSuite = parentSuite;
+    this._env = env;
+  }
+
+  DeprecatingSuiteProxyHandler.prototype.get = function(
+    target,
+    prop,
+    receiver
+  ) {
+    if (prop === 'children') {
+      if (!this._children) {
+        this._children = target.children.map(
+          this._proxyForChild.bind(this, receiver)
+        );
+      }
+
+      return this._children;
+    } else if (prop === 'parentSuite') {
+      return this._parentSuite;
+    } else {
+      this._maybeDeprecate(target, prop);
+      return target[prop];
+    }
+  };
+
+  DeprecatingSuiteProxyHandler.prototype.set = function(target, prop, value) {
+    this._maybeDeprecate(target, prop);
+    return (target[prop] = value);
+  };
+
+  DeprecatingSuiteProxyHandler.prototype._maybeDeprecate = function(
+    target,
+    prop
+  ) {
+    if (isMember(target, prop) && !isAllowedMember(prop)) {
+      this._env.deprecated(msg(prop));
+    }
+  };
+
+  DeprecatingSuiteProxyHandler.prototype._proxyForChild = function(
+    ownProxy,
+    child
+  ) {
+    if (child.children) {
+      return deprecatingSuiteProxy(child, ownProxy, this._env);
+    } else {
+      return j$.deprecatingSpecProxy(child, this._env);
+    }
+  };
+
+  function deprecatingSuiteProxy(suite, parentSuite, env) {
+    return new Proxy(suite, new DeprecatingSuiteProxyHandler(parentSuite, env));
+  }
+
+  return deprecatingSuiteProxy;
+};
+
+/* eslint-disable compat/compat */
+// TODO: Remove this in the next major release.
+getJasmineRequireObj().deprecatingThisProxy = function(j$) {
+  var msg =
+    "Access to 'this' in describe functions (and in arrow functions " +
+    'inside describe functions) is deprecated.';
+
+  try {
+    new Proxy({}, {});
+  } catch (e) {
+    // Environment does not support Poxy.
+    return function(suite) {
+      return suite;
+    };
+  }
+
+  function DeprecatingThisProxyHandler(env) {
+    this._env = env;
+  }
+
+  DeprecatingThisProxyHandler.prototype.get = function(target, prop, receiver) {
+    this._env.deprecated(msg);
+    return target[prop];
+  };
+
+  DeprecatingThisProxyHandler.prototype.set = function(target, prop, value) {
+    this._env.deprecated(msg);
+    return (target[prop] = value);
+  };
+
+  return function(suite, env) {
+    return new Proxy(suite, new DeprecatingThisProxyHandler(env));
+  };
+};
+
+getJasmineRequireObj().Deprecator = function(j$) {
+  function Deprecator(topSuite) {
+    this.topSuite_ = topSuite;
+    this.verbose_ = false;
+    this.toSuppress_ = [];
+  }
+
+  var verboseNote =
+    'Note: This message will be shown only once. Set the verboseDeprecations ' +
+    'config property to true to see every occurrence.';
+
+  Deprecator.prototype.verboseDeprecations = function(enabled) {
+    this.verbose_ = enabled;
+  };
+
+  // runnable is a spec or a suite.
+  // deprecation is a string or an Error.
+  // See Env#deprecated for a description of the options argument.
+  Deprecator.prototype.addDeprecationWarning = function(
+    runnable,
+    deprecation,
+    options
+  ) {
+    options = options || {};
+
+    if (!this.verbose_ && !j$.isError_(deprecation)) {
+      if (this.toSuppress_.indexOf(deprecation) !== -1) {
+        return;
+      }
+      this.toSuppress_.push(deprecation);
+    }
+
+    this.log_(runnable, deprecation, options);
+    this.report_(runnable, deprecation, options);
+  };
+
+  Deprecator.prototype.log_ = function(runnable, deprecation, options) {
+    var context;
+
+    if (j$.isError_(deprecation)) {
+      console.error(deprecation);
+      return;
+    }
+
+    if (runnable === this.topSuite_ || options.ignoreRunnable) {
+      context = '';
+    } else if (runnable.children) {
+      context = ' (in suite: ' + runnable.getFullName() + ')';
+    } else {
+      context = ' (in spec: ' + runnable.getFullName() + ')';
+    }
+
+    if (!options.omitStackTrace) {
+      context += '\n' + this.stackTrace_();
+    }
+
+    if (!this.verbose_) {
+      context += '\n' + verboseNote;
+    }
+
+    console.error('DEPRECATION: ' + deprecation + context);
+  };
+
+  Deprecator.prototype.stackTrace_ = function() {
+    var formatter = new j$.ExceptionFormatter();
+    return formatter.stack(j$.util.errorWithStack()).replace(/^Error\n/m, '');
+  };
+
+  Deprecator.prototype.report_ = function(runnable, deprecation, options) {
+    if (options.ignoreRunnable) {
+      runnable = this.topSuite_;
+    }
+
+    if (j$.isError_(deprecation)) {
+      runnable.addDeprecationWarning(deprecation);
+      return;
+    }
+
+    if (!this.verbose_) {
+      deprecation += '\n' + verboseNote;
+    }
+
+    runnable.addDeprecationWarning({
+      message: deprecation,
+      omitStackTrace: options.omitStackTrace || false
+    });
+  };
+
+  return Deprecator;
+};
+
+getJasmineRequireObj().errors = function() {
+  function ExpectationFailed() {}
+
+  ExpectationFailed.prototype = new Error();
+  ExpectationFailed.prototype.constructor = ExpectationFailed;
+
+  return {
+    ExpectationFailed: ExpectationFailed
+  };
+};
+
+getJasmineRequireObj().ExceptionFormatter = function(j$) {
+  var ignoredProperties = [
+    'name',
+    'message',
+    'stack',
+    'fileName',
+    'sourceURL',
+    'line',
+    'lineNumber',
+    'column',
+    'description',
+    'jasmineMessage'
+  ];
+
+  function ExceptionFormatter(options) {
+    var jasmineFile = (options && options.jasmineFile) || j$.util.jasmineFile();
+    this.message = function(error) {
+      var message = '';
+
+      if (error.jasmineMessage) {
+        message += error.jasmineMessage;
+      } else if (error.name && error.message) {
+        message += error.name + ': ' + error.message;
+      } else if (error.message) {
+        message += error.message;
+      } else {
+        message += error.toString() + ' thrown';
+      }
+
+      if (error.fileName || error.sourceURL) {
+        message += ' in ' + (error.fileName || error.sourceURL);
+      }
+
+      if (error.line || error.lineNumber) {
+        message += ' (line ' + (error.line || error.lineNumber) + ')';
+      }
+
+      return message;
+    };
+
+    this.stack = function(error) {
+      if (!error || !error.stack) {
+        return null;
+      }
+
+      var stackTrace = new j$.StackTrace(error);
+      var lines = filterJasmine(stackTrace);
+      var result = '';
+
+      if (stackTrace.message) {
+        lines.unshift(stackTrace.message);
+      }
+
+      result += formatProperties(error);
+      result += lines.join('\n');
+
+      return result;
+    };
+
+    function filterJasmine(stackTrace) {
+      var result = [],
+        jasmineMarker =
+          stackTrace.style === 'webkit' ? '<Jasmine>' : '    at <Jasmine>';
+
+      stackTrace.frames.forEach(function(frame) {
+        if (frame.file !== jasmineFile) {
+          result.push(frame.raw);
+        } else if (result[result.length - 1] !== jasmineMarker) {
+          result.push(jasmineMarker);
+        }
+      });
+
+      return result;
+    }
+
+    function formatProperties(error) {
+      if (!(error instanceof Object)) {
+        return;
+      }
+
+      var result = {};
+      var empty = true;
+
+      for (var prop in error) {
+        if (j$.util.arrayContains(ignoredProperties, prop)) {
+          continue;
+        }
+        result[prop] = error[prop];
+        empty = false;
+      }
+
+      if (!empty) {
+        return 'error properties: ' + j$.basicPrettyPrinter_(result) + '\n';
+      }
+
+      return '';
+    }
+  }
+
+  return ExceptionFormatter;
+};
+
+getJasmineRequireObj().Expectation = function(j$) {
+  /**
+   * Matchers that come with Jasmine out of the box.
+   * @namespace matchers
+   */
+  function Expectation(options) {
+    this.expector = new j$.Expector(options);
+
+    var customMatchers = options.customMatchers || {};
+    for (var matcherName in customMatchers) {
+      this[matcherName] = wrapSyncCompare(
+        matcherName,
+        customMatchers[matcherName]
+      );
+    }
+  }
+
+  /**
+   * Add some context for an {@link expect}
+   * @function
+   * @name matchers#withContext
+   * @since 3.3.0
+   * @param {String} message - Additional context to show when the matcher fails
+   * @return {matchers}
+   */
+  Expectation.prototype.withContext = function withContext(message) {
+    return addFilter(this, new ContextAddingFilter(message));
+  };
+
+  /**
+   * Invert the matcher following this {@link expect}
+   * @member
+   * @name matchers#not
+   * @since 1.3.0
+   * @type {matchers}
+   * @example
+   * expect(something).not.toBe(true);
+   */
+  Object.defineProperty(Expectation.prototype, 'not', {
+    get: function() {
+      return addFilter(this, syncNegatingFilter);
+    }
+  });
+
+  /**
+   * Asynchronous matchers that operate on an actual value which is a promise,
+   * and return a promise.
+   *
+   * Most async matchers will wait indefinitely for the promise to be resolved
+   * or rejected, resulting in a spec timeout if that never happens. If you
+   * expect that the promise will already be resolved or rejected at the time
+   * the matcher is called, you can use the {@link async-matchers#already}
+   * modifier to get a faster failure with a more helpful message.
+   *
+   * Note: Specs must await the result of each async matcher, return the
+   * promise returned by the matcher, or return a promise that's derived from
+   * the one returned by the matcher. Otherwise the matcher will not be
+   * evaluated before the spec completes.
+   *
+   * @example
+   * // Good
+   * await expectAsync(aPromise).toBeResolved();
+   * @example
+   * // Good
+   * return expectAsync(aPromise).toBeResolved();
+   * @example
+   * // Good
+   * return expectAsync(aPromise).toBeResolved()
+   *  .then(function() {
+   *    // more spec code
+   *  });
+   * @example
+   * // Bad
+   * expectAsync(aPromise).toBeResolved();
+   * @namespace async-matchers
+   */
+  function AsyncExpectation(options) {
+    var global = options.global || j$.getGlobal();
+    this.expector = new j$.Expector(options);
+
+    if (!global.Promise) {
+      throw new Error(
+        'expectAsync is unavailable because the environment does not support promises.'
+      );
+    }
+
+    var customAsyncMatchers = options.customAsyncMatchers || {};
+    for (var matcherName in customAsyncMatchers) {
+      this[matcherName] = wrapAsyncCompare(
+        matcherName,
+        customAsyncMatchers[matcherName]
+      );
+    }
+  }
+
+  /**
+   * Add some context for an {@link expectAsync}
+   * @function
+   * @name async-matchers#withContext
+   * @since 3.3.0
+   * @param {String} message - Additional context to show when the async matcher fails
+   * @return {async-matchers}
+   */
+  AsyncExpectation.prototype.withContext = function withContext(message) {
+    return addFilter(this, new ContextAddingFilter(message));
+  };
+
+  /**
+   * Invert the matcher following this {@link expectAsync}
+   * @member
+   * @name async-matchers#not
+   * @type {async-matchers}
+   * @example
+   * await expectAsync(myPromise).not.toBeResolved();
+   * @example
+   * return expectAsync(myPromise).not.toBeResolved();
+   */
+  Object.defineProperty(AsyncExpectation.prototype, 'not', {
+    get: function() {
+      return addFilter(this, asyncNegatingFilter);
+    }
+  });
+
+  /**
+   * Fail as soon as possible if the actual is pending.
+   * Otherwise evaluate the matcher.
+   * @member
+   * @name async-matchers#already
+   * @since 3.8.0
+   * @type {async-matchers}
+   * @example
+   * await expectAsync(myPromise).already.toBeResolved();
+   * @example
+   * return expectAsync(myPromise).already.toBeResolved();
+   */
+  Object.defineProperty(AsyncExpectation.prototype, 'already', {
+    get: function() {
+      return addFilter(this, expectSettledPromiseFilter);
+    }
+  });
+
+  function wrapSyncCompare(name, matcherFactory) {
+    return function() {
+      var result = this.expector.compare(name, matcherFactory, arguments);
+      this.expector.processResult(result);
+    };
+  }
+
+  function wrapAsyncCompare(name, matcherFactory) {
+    return function() {
+      var self = this;
+
+      // Capture the call stack here, before we go async, so that it will contain
+      // frames that are relevant to the user instead of just parts of Jasmine.
+      var errorForStack = j$.util.errorWithStack();
+
+      return this.expector
+        .compare(name, matcherFactory, arguments)
+        .then(function(result) {
+          self.expector.processResult(result, errorForStack);
+        });
+    };
+  }
+
+  function addCoreMatchers(prototype, matchers, wrapper) {
+    for (var matcherName in matchers) {
+      var matcher = matchers[matcherName];
+      prototype[matcherName] = wrapper(matcherName, matcher);
+    }
+  }
+
+  function addFilter(source, filter) {
+    var result = Object.create(source);
+    result.expector = source.expector.addFilter(filter);
+    return result;
+  }
+
+  function negatedFailureMessage(result, matcherName, args, matchersUtil) {
+    if (result.message) {
+      if (j$.isFunction_(result.message)) {
+        return result.message();
+      } else {
+        return result.message;
+      }
+    }
+
+    args = args.slice();
+    args.unshift(true);
+    args.unshift(matcherName);
+    return matchersUtil.buildFailureMessage.apply(matchersUtil, args);
+  }
+
+  function negate(result) {
+    result.pass = !result.pass;
+    return result;
+  }
+
+  var syncNegatingFilter = {
+    selectComparisonFunc: function(matcher) {
+      function defaultNegativeCompare() {
+        return negate(matcher.compare.apply(null, arguments));
+      }
+
+      return matcher.negativeCompare || defaultNegativeCompare;
+    },
+    buildFailureMessage: negatedFailureMessage
+  };
+
+  var asyncNegatingFilter = {
+    selectComparisonFunc: function(matcher) {
+      function defaultNegativeCompare() {
+        return matcher.compare.apply(this, arguments).then(negate);
+      }
+
+      return matcher.negativeCompare || defaultNegativeCompare;
+    },
+    buildFailureMessage: negatedFailureMessage
+  };
+
+  var expectSettledPromiseFilter = {
+    selectComparisonFunc: function(matcher) {
+      return function(actual) {
+        var matcherArgs = arguments;
+
+        return j$.isPending_(actual).then(function(isPending) {
+          if (isPending) {
+            return {
+              pass: false,
+              message:
+                'Expected a promise to be settled (via ' +
+                'expectAsync(...).already) but it was pending.'
+            };
+          } else {
+            return matcher.compare.apply(null, matcherArgs);
+          }
+        });
+      };
+    }
+  };
+
+  function ContextAddingFilter(message) {
+    this.message = message;
+  }
+
+  ContextAddingFilter.prototype.modifyFailureMessage = function(msg) {
+    var nl = msg.indexOf('\n');
+
+    if (nl === -1) {
+      return this.message + ': ' + msg;
+    } else {
+      return this.message + ':\n' + indent(msg);
+    }
+  };
+
+  function indent(s) {
+    return s.replace(/^/gm, '    ');
+  }
+
+  return {
+    factory: function(options) {
+      return new Expectation(options || {});
+    },
+    addCoreMatchers: function(matchers) {
+      addCoreMatchers(Expectation.prototype, matchers, wrapSyncCompare);
+    },
+    asyncFactory: function(options) {
+      return new AsyncExpectation(options || {});
+    },
+    addAsyncCoreMatchers: function(matchers) {
+      addCoreMatchers(AsyncExpectation.prototype, matchers, wrapAsyncCompare);
+    }
+  };
+};
+
+getJasmineRequireObj().ExpectationFilterChain = function() {
+  function ExpectationFilterChain(maybeFilter, prev) {
+    this.filter_ = maybeFilter;
+    this.prev_ = prev;
+  }
+
+  ExpectationFilterChain.prototype.addFilter = function(filter) {
+    return new ExpectationFilterChain(filter, this);
+  };
+
+  ExpectationFilterChain.prototype.selectComparisonFunc = function(matcher) {
+    return this.callFirst_('selectComparisonFunc', arguments).result;
+  };
+
+  ExpectationFilterChain.prototype.buildFailureMessage = function(
+    result,
+    matcherName,
+    args,
+    matchersUtil
+  ) {
+    return this.callFirst_('buildFailureMessage', arguments).result;
+  };
+
+  ExpectationFilterChain.prototype.modifyFailureMessage = function(msg) {
+    var result = this.callFirst_('modifyFailureMessage', arguments).result;
+    return result || msg;
+  };
+
+  ExpectationFilterChain.prototype.callFirst_ = function(fname, args) {
+    var prevResult;
+
+    if (this.prev_) {
+      prevResult = this.prev_.callFirst_(fname, args);
+
+      if (prevResult.found) {
+        return prevResult;
+      }
+    }
+
+    if (this.filter_ && this.filter_[fname]) {
+      return {
+        found: true,
+        result: this.filter_[fname].apply(this.filter_, args)
+      };
+    }
+
+    return { found: false };
+  };
+
+  return ExpectationFilterChain;
+};
+
+//TODO: expectation result may make more sense as a presentation of an expectation.
+getJasmineRequireObj().buildExpectationResult = function(j$) {
+  function buildExpectationResult(options) {
+    var messageFormatter = options.messageFormatter || function() {},
+      stackFormatter = options.stackFormatter || function() {};
+
+    /**
+     * @typedef Expectation
+     * @property {String} matcherName - The name of the matcher that was executed for this expectation.
+     * @property {String} message - The failure message for the expectation.
+     * @property {String} stack - The stack trace for the failure if available.
+     * @property {Boolean} passed - Whether the expectation passed or failed.
+     * @property {Object} expected - If the expectation failed, what was the expected value.
+     * @property {Object} actual - If the expectation failed, what actual value was produced.
+     */
+    var result = {
+      matcherName: options.matcherName,
+      message: message(),
+      stack: options.omitStackTrace ? '' : stack(),
+      passed: options.passed
+    };
+
+    if (!result.passed) {
+      result.expected = options.expected;
+      result.actual = options.actual;
+
+      if (options.error && !j$.isString_(options.error)) {
+        if ('code' in options.error) {
+          result.code = options.error.code;
+        }
+
+        if (
+          options.error.code === 'ERR_ASSERTION' &&
+          options.expected === '' &&
+          options.actual === ''
+        ) {
+          result.expected = options.error.expected;
+          result.actual = options.error.actual;
+          result.matcherName = 'assert ' + options.error.operator;
+        }
+      }
+    }
+
+    return result;
+
+    function message() {
+      if (options.passed) {
+        return 'Passed.';
+      } else if (options.message) {
+        return options.message;
+      } else if (options.error) {
+        return messageFormatter(options.error);
+      }
+      return '';
+    }
+
+    function stack() {
+      if (options.passed) {
+        return '';
+      }
+
+      var error = options.error;
+      if (!error) {
+        if (options.errorForStack) {
+          error = options.errorForStack;
+        } else if (options.stack) {
+          error = options;
+        } else {
+          try {
+            throw new Error(message());
+          } catch (e) {
+            error = e;
+          }
+        }
+      }
+      return stackFormatter(error);
+    }
+  }
+
+  return buildExpectationResult;
+};
+
+getJasmineRequireObj().Expector = function(j$) {
+  function Expector(options) {
+    this.matchersUtil = options.matchersUtil || {
+      buildFailureMessage: function() {}
+    };
+    this.customEqualityTesters = options.customEqualityTesters || [];
+    this.actual = options.actual;
+    this.addExpectationResult = options.addExpectationResult || function() {};
+    this.filters = new j$.ExpectationFilterChain();
+  }
+
+  Expector.prototype.instantiateMatcher = function(
+    matcherName,
+    matcherFactory,
+    args
+  ) {
+    this.matcherName = matcherName;
+    this.args = Array.prototype.slice.call(args, 0);
+    this.expected = this.args.slice(0);
+
+    this.args.unshift(this.actual);
+
+    // TODO: Remove support for passing customEqualityTesters in the next major release.
+    var matcher;
+
+    if (matcherFactory.length >= 2) {
+      matcher = matcherFactory(this.matchersUtil, this.customEqualityTesters);
+    } else {
+      matcher = matcherFactory(this.matchersUtil);
+    }
+
+    var comparisonFunc = this.filters.selectComparisonFunc(matcher);
+    return comparisonFunc || matcher.compare;
+  };
+
+  Expector.prototype.buildMessage = function(result) {
+    var self = this;
+
+    if (result.pass) {
+      return '';
+    }
+
+    var msg = this.filters.buildFailureMessage(
+      result,
+      this.matcherName,
+      this.args,
+      this.matchersUtil,
+      defaultMessage
+    );
+    return this.filters.modifyFailureMessage(msg || defaultMessage());
+
+    function defaultMessage() {
+      if (!result.message) {
+        var args = self.args.slice();
+        args.unshift(false);
+        args.unshift(self.matcherName);
+        return self.matchersUtil.buildFailureMessage.apply(
+          self.matchersUtil,
+          args
+        );
+      } else if (j$.isFunction_(result.message)) {
+        return result.message();
+      } else {
+        return result.message;
+      }
+    }
+  };
+
+  Expector.prototype.compare = function(matcherName, matcherFactory, args) {
+    var matcherCompare = this.instantiateMatcher(
+      matcherName,
+      matcherFactory,
+      args
+    );
+    return matcherCompare.apply(null, this.args);
+  };
+
+  Expector.prototype.addFilter = function(filter) {
+    var result = Object.create(this);
+    result.filters = this.filters.addFilter(filter);
+    return result;
+  };
+
+  Expector.prototype.processResult = function(result, errorForStack) {
+    var message = this.buildMessage(result);
+
+    if (this.expected.length === 1) {
+      this.expected = this.expected[0];
+    }
+
+    this.addExpectationResult(result.pass, {
+      matcherName: this.matcherName,
+      passed: result.pass,
+      message: message,
+      error: errorForStack ? undefined : result.error,
+      errorForStack: errorForStack || undefined,
+      actual: this.actual,
+      expected: this.expected // TODO: this may need to be arrayified/sliced
+    });
+  };
+
+  return Expector;
+};
+
+getJasmineRequireObj().formatErrorMsg = function() {
+  function generateErrorMsg(domain, usage) {
+    var usageDefinition = usage ? '\nUsage: ' + usage : '';
+
+    return function errorMsg(msg) {
+      return domain + ' : ' + msg + usageDefinition;
+    };
+  }
+
+  return generateErrorMsg;
+};
+
+getJasmineRequireObj().GlobalErrors = function(j$) {
+  function GlobalErrors(global) {
+    var handlers = [];
+    global = global || j$.getGlobal();
+
+    var onerror = function onerror() {
+      var handler = handlers[handlers.length - 1];
+
+      if (handler) {
+        handler.apply(null, Array.prototype.slice.call(arguments, 0));
+      } else {
+        throw arguments[0];
+      }
+    };
+
+    this.originalHandlers = {};
+    this.jasmineHandlers = {};
+    this.installOne_ = function installOne_(errorType, jasmineMessage) {
+      function taggedOnError(error) {
+        var substituteMsg;
+
+        if (j$.isError_(error)) {
+          error.jasmineMessage = jasmineMessage + ': ' + error;
+        } else {
+          if (error) {
+            substituteMsg = jasmineMessage + ': ' + error;
+          } else {
+            substituteMsg = jasmineMessage + ' with no error or message';
+          }
+
+          if (errorType === 'unhandledRejection') {
+            substituteMsg +=
+              '\n' +
+              '(Tip: to get a useful stack trace, use ' +
+              'Promise.reject(new Error(...)) instead of Promise.reject(' +
+              (error ? '...' : '') +
+              ').)';
+          }
+
+          error = new Error(substituteMsg);
+        }
+
+        var handler = handlers[handlers.length - 1];
+
+        if (handler) {
+          handler(error);
+        } else {
+          throw error;
+        }
+      }
+
+      this.originalHandlers[errorType] = global.process.listeners(errorType);
+      this.jasmineHandlers[errorType] = taggedOnError;
+
+      global.process.removeAllListeners(errorType);
+      global.process.on(errorType, taggedOnError);
+
+      this.uninstall = function uninstall() {
+        var errorTypes = Object.keys(this.originalHandlers);
+        for (var iType = 0; iType < errorTypes.length; iType++) {
+          var errorType = errorTypes[iType];
+          global.process.removeListener(
+            errorType,
+            this.jasmineHandlers[errorType]
+          );
+          for (var i = 0; i < this.originalHandlers[errorType].length; i++) {
+            global.process.on(errorType, this.originalHandlers[errorType][i]);
+          }
+          delete this.originalHandlers[errorType];
+          delete this.jasmineHandlers[errorType];
+        }
+      };
+    };
+
+    this.install = function install() {
+      if (
+        global.process &&
+        global.process.listeners &&
+        j$.isFunction_(global.process.on)
+      ) {
+        this.installOne_('uncaughtException', 'Uncaught exception');
+        this.installOne_('unhandledRejection', 'Unhandled promise rejection');
+      } else {
+        var originalHandler = global.onerror;
+        global.onerror = onerror;
+
+        var browserRejectionHandler = function browserRejectionHandler(event) {
+          if (j$.isError_(event.reason)) {
+            event.reason.jasmineMessage =
+              'Unhandled promise rejection: ' + event.reason;
+            global.onerror(event.reason);
+          } else {
+            global.onerror('Unhandled promise rejection: ' + event.reason);
+          }
+        };
+
+        if (global.addEventListener) {
+          global.addEventListener(
+            'unhandledrejection',
+            browserRejectionHandler
+          );
+        }
+
+        this.uninstall = function uninstall() {
+          global.onerror = originalHandler;
+          if (global.removeEventListener) {
+            global.removeEventListener(
+              'unhandledrejection',
+              browserRejectionHandler
+            );
+          }
+        };
+      }
+    };
+
+    this.pushListener = function pushListener(listener) {
+      handlers.push(listener);
+    };
+
+    this.popListener = function popListener(listener) {
+      if (!listener) {
+        throw new Error('popListener expects a listener');
+      }
+
+      handlers.pop();
+    };
+  }
+
+  return GlobalErrors;
+};
+
+/* eslint-disable compat/compat */
+getJasmineRequireObj().toBePending = function(j$) {
+  /**
+   * Expect a promise to be pending, i.e. the promise is neither resolved nor rejected.
+   * @function
+   * @async
+   * @name async-matchers#toBePending
+   * @since 3.6
+   * @example
+   * await expectAsync(aPromise).toBePending();
+   */
+  return function toBePending() {
+    return {
+      compare: function(actual) {
+        if (!j$.isPromiseLike(actual)) {
+          throw new Error('Expected toBePending to be called on a promise.');
+        }
+        var want = {};
+        return Promise.race([actual, Promise.resolve(want)]).then(
+          function(got) {
+            return { pass: want === got };
+          },
+          function() {
+            return { pass: false };
+          }
+        );
+      }
+    };
+  };
+};
+
+getJasmineRequireObj().toBeRejected = function(j$) {
+  /**
+   * Expect a promise to be rejected.
+   * @function
+   * @async
+   * @name async-matchers#toBeRejected
+   * @since 3.1.0
+   * @example
+   * await expectAsync(aPromise).toBeRejected();
+   * @example
+   * return expectAsync(aPromise).toBeRejected();
+   */
+  return function toBeRejected() {
+    return {
+      compare: function(actual) {
+        if (!j$.isPromiseLike(actual)) {
+          throw new Error('Expected toBeRejected to be called on a promise.');
+        }
+        return actual.then(
+          function() {
+            return { pass: false };
+          },
+          function() {
+            return { pass: true };
+          }
+        );
+      }
+    };
+  };
+};
+
+getJasmineRequireObj().toBeRejectedWith = function(j$) {
+  /**
+   * Expect a promise to be rejected with a value equal to the expected, using deep equality comparison.
+   * @function
+   * @async
+   * @name async-matchers#toBeRejectedWith
+   * @since 3.3.0
+   * @param {Object} expected - Value that the promise is expected to be rejected with
+   * @example
+   * await expectAsync(aPromise).toBeRejectedWith({prop: 'value'});
+   * @example
+   * return expectAsync(aPromise).toBeRejectedWith({prop: 'value'});
+   */
+  return function toBeRejectedWith(matchersUtil) {
+    return {
+      compare: function(actualPromise, expectedValue) {
+        if (!j$.isPromiseLike(actualPromise)) {
+          throw new Error(
+            'Expected toBeRejectedWith to be called on a promise.'
+          );
+        }
+
+        function prefix(passed) {
+          return (
+            'Expected a promise ' +
+            (passed ? 'not ' : '') +
+            'to be rejected with ' +
+            matchersUtil.pp(expectedValue)
+          );
+        }
+
+        return actualPromise.then(
+          function() {
+            return {
+              pass: false,
+              message: prefix(false) + ' but it was resolved.'
+            };
+          },
+          function(actualValue) {
+            if (matchersUtil.equals(actualValue, expectedValue)) {
+              return {
+                pass: true,
+                message: prefix(true) + '.'
+              };
+            } else {
+              return {
+                pass: false,
+                message:
+                  prefix(false) +
+                  ' but it was rejected with ' +
+                  matchersUtil.pp(actualValue) +
+                  '.'
+              };
+            }
+          }
+        );
+      }
+    };
+  };
+};
+
+getJasmineRequireObj().toBeRejectedWithError = function(j$) {
+  /**
+   * Expect a promise to be rejected with a value matched to the expected
+   * @function
+   * @async
+   * @name async-matchers#toBeRejectedWithError
+   * @since 3.5.0
+   * @param {Error} [expected] - `Error` constructor the object that was thrown needs to be an instance of. If not provided, `Error` will be used.
+   * @param {RegExp|String} [message] - The message that should be set on the thrown `Error`
+   * @example
+   * await expectAsync(aPromise).toBeRejectedWithError(MyCustomError, 'Error message');
+   * await expectAsync(aPromise).toBeRejectedWithError(MyCustomError, /Error message/);
+   * await expectAsync(aPromise).toBeRejectedWithError(MyCustomError);
+   * await expectAsync(aPromise).toBeRejectedWithError('Error message');
+   * return expectAsync(aPromise).toBeRejectedWithError(/Error message/);
+   */
+  return function toBeRejectedWithError(matchersUtil) {
+    return {
+      compare: function(actualPromise, arg1, arg2) {
+        if (!j$.isPromiseLike(actualPromise)) {
+          throw new Error(
+            'Expected toBeRejectedWithError to be called on a promise.'
+          );
+        }
+
+        var expected = getExpectedFromArgs(arg1, arg2, matchersUtil);
+
+        return actualPromise.then(
+          function() {
+            return {
+              pass: false,
+              message: 'Expected a promise to be rejected but it was resolved.'
+            };
+          },
+          function(actualValue) {
+            return matchError(actualValue, expected, matchersUtil);
+          }
+        );
+      }
+    };
+  };
+
+  function matchError(actual, expected, matchersUtil) {
+    if (!j$.isError_(actual)) {
+      return fail(expected, 'rejected with ' + matchersUtil.pp(actual));
+    }
+
+    if (!(actual instanceof expected.error)) {
+      return fail(
+        expected,
+        'rejected with type ' + j$.fnNameFor(actual.constructor)
+      );
+    }
+
+    var actualMessage = actual.message;
+
+    if (
+      actualMessage === expected.message ||
+      typeof expected.message === 'undefined'
+    ) {
+      return pass(expected);
+    }
+
+    if (
+      expected.message instanceof RegExp &&
+      expected.message.test(actualMessage)
+    ) {
+      return pass(expected);
+    }
+
+    return fail(expected, 'rejected with ' + matchersUtil.pp(actual));
+  }
+
+  function pass(expected) {
+    return {
+      pass: true,
+      message:
+        'Expected a promise not to be rejected with ' +
+        expected.printValue +
+        ', but it was.'
+    };
+  }
+
+  function fail(expected, message) {
+    return {
+      pass: false,
+      message:
+        'Expected a promise to be rejected with ' +
+        expected.printValue +
+        ' but it was ' +
+        message +
+        '.'
+    };
+  }
+
+  function getExpectedFromArgs(arg1, arg2, matchersUtil) {
+    var error, message;
+
+    if (isErrorConstructor(arg1)) {
+      error = arg1;
+      message = arg2;
+    } else {
+      error = Error;
+      message = arg1;
+    }
+
+    return {
+      error: error,
+      message: message,
+      printValue:
+        j$.fnNameFor(error) +
+        (typeof message === 'undefined' ? '' : ': ' + matchersUtil.pp(message))
+    };
+  }
+
+  function isErrorConstructor(value) {
+    return (
+      typeof value === 'function' &&
+      (value === Error || j$.isError_(value.prototype))
+    );
+  }
+};
+
+getJasmineRequireObj().toBeResolved = function(j$) {
+  /**
+   * Expect a promise to be resolved.
+   * @function
+   * @async
+   * @name async-matchers#toBeResolved
+   * @since 3.1.0
+   * @example
+   * await expectAsync(aPromise).toBeResolved();
+   * @example
+   * return expectAsync(aPromise).toBeResolved();
+   */
+  return function toBeResolved(matchersUtil) {
+    return {
+      compare: function(actual) {
+        if (!j$.isPromiseLike(actual)) {
+          throw new Error('Expected toBeResolved to be called on a promise.');
+        }
+
+        return actual.then(
+          function() {
+            return { pass: true };
+          },
+          function(e) {
+            return {
+              pass: false,
+              message:
+                'Expected a promise to be resolved but it was ' +
+                'rejected with ' +
+                matchersUtil.pp(e) +
+                '.'
+            };
+          }
+        );
+      }
+    };
+  };
+};
+
+getJasmineRequireObj().toBeResolvedTo = function(j$) {
+  /**
+   * Expect a promise to be resolved to a value equal to the expected, using deep equality comparison.
+   * @function
+   * @async
+   * @name async-matchers#toBeResolvedTo
+   * @since 3.1.0
+   * @param {Object} expected - Value that the promise is expected to resolve to
+   * @example
+   * await expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
+   * @example
+   * return expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
+   */
+  return function toBeResolvedTo(matchersUtil) {
+    return {
+      compare: function(actualPromise, expectedValue) {
+        if (!j$.isPromiseLike(actualPromise)) {
+          throw new Error('Expected toBeResolvedTo to be called on a promise.');
+        }
+
+        function prefix(passed) {
+          return (
+            'Expected a promise ' +
+            (passed ? 'not ' : '') +
+            'to be resolved to ' +
+            matchersUtil.pp(expectedValue)
+          );
+        }
+
+        return actualPromise.then(
+          function(actualValue) {
+            if (matchersUtil.equals(actualValue, expectedValue)) {
+              return {
+                pass: true,
+                message: prefix(true) + '.'
+              };
+            } else {
+              return {
+                pass: false,
+                message:
+                  prefix(false) +
+                  ' but it was resolved to ' +
+                  matchersUtil.pp(actualValue) +
+                  '.'
+              };
+            }
+          },
+          function(e) {
+            return {
+              pass: false,
+              message:
+                prefix(false) +
+                ' but it was rejected with ' +
+                matchersUtil.pp(e) +
+                '.'
+            };
+          }
+        );
+      }
+    };
+  };
+};
+
+getJasmineRequireObj().DiffBuilder = function(j$) {
+  return function DiffBuilder(config) {
+    var prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter(),
+      mismatches = new j$.MismatchTree(),
+      path = new j$.ObjectPath(),
+      actualRoot = undefined,
+      expectedRoot = undefined;
+
+    return {
+      setRoots: function(actual, expected) {
+        actualRoot = actual;
+        expectedRoot = expected;
+      },
+
+      recordMismatch: function(formatter) {
+        mismatches.add(path, formatter);
+      },
+
+      getMessage: function() {
+        var messages = [];
+
+        mismatches.traverse(function(path, isLeaf, formatter) {
+          var actualCustom,
+            expectedCustom,
+            useCustom,
+            derefResult = dereferencePath(
+              path,
+              actualRoot,
+              expectedRoot,
+              prettyPrinter
+            ),
+            actual = derefResult.actual,
+            expected = derefResult.expected;
+
+          if (formatter) {
+            messages.push(formatter(actual, expected, path, prettyPrinter));
+            return true;
+          }
+
+          actualCustom = prettyPrinter.customFormat_(actual);
+          expectedCustom = prettyPrinter.customFormat_(expected);
+          useCustom = !(
+            j$.util.isUndefined(actualCustom) &&
+            j$.util.isUndefined(expectedCustom)
+          );
+
+          if (useCustom) {
+            messages.push(
+              wrapPrettyPrinted(actualCustom, expectedCustom, path)
+            );
+            return false; // don't recurse further
+          }
+
+          if (isLeaf) {
+            messages.push(
+              defaultFormatter(actual, expected, path, prettyPrinter)
+            );
+          }
+
+          return true;
+        });
+
+        return messages.join('\n');
+      },
+
+      withPath: function(pathComponent, block) {
+        var oldPath = path;
+        path = path.add(pathComponent);
+        block();
+        path = oldPath;
+      }
+    };
+
+    function defaultFormatter(actual, expected, path, prettyPrinter) {
+      return wrapPrettyPrinted(
+        prettyPrinter(actual),
+        prettyPrinter(expected),
+        path
+      );
+    }
+
+    function wrapPrettyPrinted(actual, expected, path) {
+      return (
+        'Expected ' +
+        path +
+        (path.depth() ? ' = ' : '') +
+        actual +
+        ' to equal ' +
+        expected +
+        '.'
+      );
+    }
+  };
+
+  function dereferencePath(objectPath, actual, expected, pp) {
+    function handleAsymmetricExpected() {
+      if (
+        j$.isAsymmetricEqualityTester_(expected) &&
+        j$.isFunction_(expected.valuesForDiff_)
+      ) {
+        var asymmetricResult = expected.valuesForDiff_(actual, pp);
+        expected = asymmetricResult.self;
+        actual = asymmetricResult.other;
+      }
+    }
+
+    var i;
+    handleAsymmetricExpected();
+
+    for (i = 0; i < objectPath.components.length; i++) {
+      actual = actual[objectPath.components[i]];
+      expected = expected[objectPath.components[i]];
+      handleAsymmetricExpected();
+    }
+
+    return { actual: actual, expected: expected };
+  }
+};
+
+getJasmineRequireObj().MatchersUtil = function(j$) {
+  /**
+   * @class MatchersUtil
+   * @classdesc Utilities for use in implementing matchers.<br>
+   * _Note:_ Do not construct this directly. Jasmine will construct one and
+   * pass it to matchers and asymmetric equality testers.
+   * @hideconstructor
+   */
+  function MatchersUtil(options) {
+    options = options || {};
+    this.customTesters_ = options.customTesters || [];
+    /**
+     * Formats a value for use in matcher failure messages and similar contexts,
+     * taking into account the current set of custom value formatters.
+     * @function
+     * @name MatchersUtil#pp
+     * @since 3.6.0
+     * @param {*} value The value to pretty-print
+     * @return {string} The pretty-printed value
+     */
+    this.pp = options.pp || function() {};
+  }
+
+  /**
+   * Determines whether `haystack` contains `needle`, using the same comparison
+   * logic as {@link MatchersUtil#equals}.
+   * @function
+   * @name MatchersUtil#contains
+   * @since 2.0.0
+   * @param {*} haystack The collection to search
+   * @param {*} needle The value to search for
+   * @param [customTesters] An array of custom equality testers. Deprecated.
+   * As of 3.6 this parameter no longer needs to be passed. It will be removed in 4.0.
+   * @returns {boolean} True if `needle` was found in `haystack`
+   */
+  MatchersUtil.prototype.contains = function(haystack, needle, customTesters) {
+    if (customTesters) {
+      j$.getEnv().deprecated(
+        'Passing custom equality testers ' +
+          'to MatchersUtil#contains is deprecated. ' +
+          'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#matchers-cet> for details.'
+      );
+    }
+
+    if (j$.isSet(haystack)) {
+      return haystack.has(needle);
+    }
+
+    if (
+      Object.prototype.toString.apply(haystack) === '[object Array]' ||
+      (!!haystack && !haystack.indexOf)
+    ) {
+      for (var i = 0; i < haystack.length; i++) {
+        try {
+          this.suppressDeprecation_ = true;
+          if (this.equals(haystack[i], needle, customTesters)) {
+            return true;
+          }
+        } finally {
+          this.suppressDeprecation_ = false;
+        }
+      }
+      return false;
+    }
+
+    return !!haystack && haystack.indexOf(needle) >= 0;
+  };
+
+  MatchersUtil.prototype.buildFailureMessage = function() {
+    var self = this;
+    var args = Array.prototype.slice.call(arguments, 0),
+      matcherName = args[0],
+      isNot = args[1],
+      actual = args[2],
+      expected = args.slice(3),
+      englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) {
+        return ' ' + s.toLowerCase();
+      });
+
+    var message =
+      'Expected ' +
+      self.pp(actual) +
+      (isNot ? ' not ' : ' ') +
+      englishyPredicate;
+
+    if (expected.length > 0) {
+      for (var i = 0; i < expected.length; i++) {
+        if (i > 0) {
+          message += ',';
+        }
+        message += ' ' + self.pp(expected[i]);
+      }
+    }
+
+    return message + '.';
+  };
+
+  MatchersUtil.prototype.asymmetricDiff_ = function(
+    a,
+    b,
+    aStack,
+    bStack,
+    customTesters,
+    diffBuilder
+  ) {
+    if (j$.isFunction_(b.valuesForDiff_)) {
+      var values = b.valuesForDiff_(a, this.pp);
+      this.eq_(
+        values.other,
+        values.self,
+        aStack,
+        bStack,
+        customTesters,
+        diffBuilder
+      );
+    } else {
+      diffBuilder.recordMismatch();
+    }
+  };
+
+  MatchersUtil.prototype.asymmetricMatch_ = function(
+    a,
+    b,
+    aStack,
+    bStack,
+    customTesters,
+    diffBuilder
+  ) {
+    var asymmetricA = j$.isAsymmetricEqualityTester_(a),
+      asymmetricB = j$.isAsymmetricEqualityTester_(b),
+      shim,
+      result;
+
+    if (asymmetricA === asymmetricB) {
+      return undefined;
+    }
+
+    shim = j$.asymmetricEqualityTesterArgCompatShim(this, customTesters);
+
+    if (asymmetricA) {
+      result = a.asymmetricMatch(b, shim);
+      if (!result) {
+        diffBuilder.recordMismatch();
+      }
+      return result;
+    }
+
+    if (asymmetricB) {
+      result = b.asymmetricMatch(a, shim);
+      if (!result) {
+        this.asymmetricDiff_(a, b, aStack, bStack, customTesters, diffBuilder);
+      }
+      return result;
+    }
+  };
+
+  /**
+   * Determines whether two values are deeply equal to each other.
+   * @function
+   * @name MatchersUtil#equals
+   * @since 2.0.0
+   * @param {*} a The first value to compare
+   * @param {*} b The second value to compare
+   * @param [customTesters] An array of custom equality testers. Deprecated.
+   * As of 3.6 this parameter no longer needs to be passed. It will be removed in 4.0.
+   * @returns {boolean} True if the values are equal
+   */
+  MatchersUtil.prototype.equals = function(
+    a,
+    b,
+    customTestersOrDiffBuilder,
+    diffBuilderOrNothing
+  ) {
+    var customTesters, diffBuilder;
+
+    if (isDiffBuilder(customTestersOrDiffBuilder)) {
+      diffBuilder = customTestersOrDiffBuilder;
+    } else {
+      if (customTestersOrDiffBuilder && !this.suppressDeprecation_) {
+        j$.getEnv().deprecated(
+          'Passing custom equality testers ' +
+            'to MatchersUtil#equals is deprecated. ' +
+            'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#matchers-cet> for details.'
+        );
+      }
+
+      if (diffBuilderOrNothing) {
+        j$.getEnv().deprecated(
+          'Diff builder should be passed ' +
+            'as the third argument to MatchersUtil#equals, not the fourth. ' +
+            'See <https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#matchers-cet> for details.'
+        );
+      }
+
+      customTesters = customTestersOrDiffBuilder;
+      diffBuilder = diffBuilderOrNothing;
+    }
+
+    customTesters = customTesters || this.customTesters_;
+    diffBuilder = diffBuilder || j$.NullDiffBuilder();
+    diffBuilder.setRoots(a, b);
+
+    return this.eq_(a, b, [], [], customTesters, diffBuilder);
+  };
+
+  // Equality function lovingly adapted from isEqual in
+  //   [Underscore](http://underscorejs.org)
+  MatchersUtil.prototype.eq_ = function(
+    a,
+    b,
+    aStack,
+    bStack,
+    customTesters,
+    diffBuilder
+  ) {
+    var result = true,
+      self = this,
+      i;
+
+    var asymmetricResult = this.asymmetricMatch_(
+      a,
+      b,
+      aStack,
+      bStack,
+      customTesters,
+      diffBuilder
+    );
+    if (!j$.util.isUndefined(asymmetricResult)) {
+      return asymmetricResult;
+    }
+
+    for (i = 0; i < customTesters.length; i++) {
+      var customTesterResult = customTesters[i](a, b);
+      if (!j$.util.isUndefined(customTesterResult)) {
+        if (!customTesterResult) {
+          diffBuilder.recordMismatch();
+        }
+        return customTesterResult;
+      }
+    }
+
+    if (a instanceof Error && b instanceof Error) {
+      result = a.message == b.message;
+      if (!result) {
+        diffBuilder.recordMismatch();
+      }
+      return result;
+    }
+
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) {
+      result = a !== 0 || 1 / a == 1 / b;
+      if (!result) {
+        diffBuilder.recordMismatch();
+      }
+      return result;
+    }
+    // A strict comparison is necessary because `null == undefined`.
+    if (a === null || b === null) {
+      result = a === b;
+      if (!result) {
+        diffBuilder.recordMismatch();
+      }
+      return result;
+    }
+    var className = Object.prototype.toString.call(a);
+    if (className != Object.prototype.toString.call(b)) {
+      diffBuilder.recordMismatch();
+      return false;
+    }
+    switch (className) {
+      // Strings, numbers, dates, and booleans are compared by value.
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        result = a == String(b);
+        if (!result) {
+          diffBuilder.recordMismatch();
+        }
+        return result;
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+        // other numeric values.
+        result =
+          a != +a ? b != +b : a === 0 && b === 0 ? 1 / a == 1 / b : a == +b;
+        if (!result) {
+          diffBuilder.recordMismatch();
+        }
+        return result;
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        result = +a == +b;
+        if (!result) {
+          diffBuilder.recordMismatch();
+        }
+        return result;
+      case '[object ArrayBuffer]':
+        // If we have an instance of ArrayBuffer the Uint8Array ctor
+        // will be defined as well
+        return self.eq_(
+          new Uint8Array(a), // eslint-disable-line compat/compat
+          new Uint8Array(b), // eslint-disable-line compat/compat
+          aStack,
+          bStack,
+          customTesters,
+          diffBuilder
+        );
+      // RegExps are compared by their source patterns and flags.
+      case '[object RegExp]':
+        return (
+          a.source == b.source &&
+          a.global == b.global &&
+          a.multiline == b.multiline &&
+          a.ignoreCase == b.ignoreCase
+        );
+    }
+    if (typeof a != 'object' || typeof b != 'object') {
+      diffBuilder.recordMismatch();
+      return false;
+    }
+
+    var aIsDomNode = j$.isDomNode(a);
+    var bIsDomNode = j$.isDomNode(b);
+    if (aIsDomNode && bIsDomNode) {
+      // At first try to use DOM3 method isEqualNode
+      result = a.isEqualNode(b);
+      if (!result) {
+        diffBuilder.recordMismatch();
+      }
+      return result;
+    }
+    if (aIsDomNode || bIsDomNode) {
+      diffBuilder.recordMismatch();
+      return false;
+    }
+
+    var aIsPromise = j$.isPromise(a);
+    var bIsPromise = j$.isPromise(b);
+    if (aIsPromise && bIsPromise) {
+      return a === b;
+    }
+
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] == a) {
+        return bStack[length] == b;
+      }
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size = 0;
+    // Recursively compare objects and arrays.
+    // Compare array lengths to determine if a deep comparison is necessary.
+    if (className == '[object Array]') {
+      var aLength = a.length;
+      var bLength = b.length;
+
+      diffBuilder.withPath('length', function() {
+        if (aLength !== bLength) {
+          diffBuilder.recordMismatch();
+          result = false;
+        }
+      });
+
+      for (i = 0; i < aLength || i < bLength; i++) {
+        diffBuilder.withPath(i, function() {
+          if (i >= bLength) {
+            diffBuilder.recordMismatch(
+              actualArrayIsLongerFormatter.bind(null, self.pp)
+            );
+            result = false;
+          } else {
+            result =
+              self.eq_(
+                i < aLength ? a[i] : void 0,
+                i < bLength ? b[i] : void 0,
+                aStack,
+                bStack,
+                customTesters,
+                diffBuilder
+              ) && result;
+          }
+        });
+      }
+      if (!result) {
+        return false;
+      }
+    } else if (j$.isMap(a) && j$.isMap(b)) {
+      if (a.size != b.size) {
+        diffBuilder.recordMismatch();
+        return false;
+      }
+
+      var keysA = [];
+      var keysB = [];
+      a.forEach(function(valueA, keyA) {
+        keysA.push(keyA);
+      });
+      b.forEach(function(valueB, keyB) {
+        keysB.push(keyB);
+      });
+
+      // For both sets of keys, check they map to equal values in both maps.
+      // Keep track of corresponding keys (in insertion order) in order to handle asymmetric obj keys.
+      var mapKeys = [keysA, keysB];
+      var cmpKeys = [keysB, keysA];
+      var mapIter, mapKey, mapValueA, mapValueB;
+      var cmpIter, cmpKey;
+      for (i = 0; result && i < mapKeys.length; i++) {
+        mapIter = mapKeys[i];
+        cmpIter = cmpKeys[i];
+
+        for (var j = 0; result && j < mapIter.length; j++) {
+          mapKey = mapIter[j];
+          cmpKey = cmpIter[j];
+          mapValueA = a.get(mapKey);
+
+          // Only use the cmpKey when one of the keys is asymmetric and the corresponding key matches,
+          // otherwise explicitly look up the mapKey in the other Map since we want keys with unique
+          // obj identity (that are otherwise equal) to not match.
+          if (
+            j$.isAsymmetricEqualityTester_(mapKey) ||
+            (j$.isAsymmetricEqualityTester_(cmpKey) &&
+              this.eq_(
+                mapKey,
+                cmpKey,
+                aStack,
+                bStack,
+                customTesters,
+                j$.NullDiffBuilder()
+              ))
+          ) {
+            mapValueB = b.get(cmpKey);
+          } else {
+            mapValueB = b.get(mapKey);
+          }
+          result = this.eq_(
+            mapValueA,
+            mapValueB,
+            aStack,
+            bStack,
+            customTesters,
+            j$.NullDiffBuilder()
+          );
+        }
+      }
+
+      if (!result) {
+        diffBuilder.recordMismatch();
+        return false;
+      }
+    } else if (j$.isSet(a) && j$.isSet(b)) {
+      if (a.size != b.size) {
+        diffBuilder.recordMismatch();
+        return false;
+      }
+
+      var valuesA = [];
+      a.forEach(function(valueA) {
+        valuesA.push(valueA);
+      });
+      var valuesB = [];
+      b.forEach(function(valueB) {
+        valuesB.push(valueB);
+      });
+
+      // For both sets, check they are all contained in the other set
+      var setPairs = [[valuesA, valuesB], [valuesB, valuesA]];
+      var stackPairs = [[aStack, bStack], [bStack, aStack]];
+      var baseValues, baseValue, baseStack;
+      var otherValues, otherValue, otherStack;
+      var found;
+      var prevStackSize;
+      for (i = 0; result && i < setPairs.length; i++) {
+        baseValues = setPairs[i][0];
+        otherValues = setPairs[i][1];
+        baseStack = stackPairs[i][0];
+        otherStack = stackPairs[i][1];
+        // For each value in the base set...
+        for (var k = 0; result && k < baseValues.length; k++) {
+          baseValue = baseValues[k];
+          found = false;
+          // ... test that it is present in the other set
+          for (var l = 0; !found && l < otherValues.length; l++) {
+            otherValue = otherValues[l];
+            prevStackSize = baseStack.length;
+            // compare by value equality
+            found = this.eq_(
+              baseValue,
+              otherValue,
+              baseStack,
+              otherStack,
+              customTesters,
+              j$.NullDiffBuilder()
+            );
+            if (!found && prevStackSize !== baseStack.length) {
+              baseStack.splice(prevStackSize);
+              otherStack.splice(prevStackSize);
+            }
+          }
+          result = result && found;
+        }
+      }
+
+      if (!result) {
+        diffBuilder.recordMismatch();
+        return false;
+      }
+    } else if (j$.isURL(a) && j$.isURL(b)) {
+      // URLs have no enumrable properties, so the default object comparison
+      // would consider any two URLs to be equal.
+      return a.toString() === b.toString();
+    } else {
+      // Objects with different constructors are not equivalent, but `Object`s
+      // or `Array`s from different frames are.
+      var aCtor = a.constructor,
+        bCtor = b.constructor;
+      if (
+        aCtor !== bCtor &&
+        isFunction(aCtor) &&
+        isFunction(bCtor) &&
+        a instanceof aCtor &&
+        b instanceof bCtor &&
+        !(aCtor instanceof aCtor && bCtor instanceof bCtor)
+      ) {
+        diffBuilder.recordMismatch(
+          constructorsAreDifferentFormatter.bind(null, this.pp)
+        );
+        return false;
+      }
+    }
+
+    // Deep compare objects.
+    var aKeys = keys(a, className == '[object Array]'),
+      key;
+    size = aKeys.length;
+
+    // Ensure that both objects contain the same number of properties before comparing deep equality.
+    if (keys(b, className == '[object Array]').length !== size) {
+      diffBuilder.recordMismatch(
+        objectKeysAreDifferentFormatter.bind(null, this.pp)
+      );
+      return false;
+    }
+
+    for (i = 0; i < size; i++) {
+      key = aKeys[i];
+      // Deep compare each member
+      if (!j$.util.has(b, key)) {
+        diffBuilder.recordMismatch(
+          objectKeysAreDifferentFormatter.bind(null, this.pp)
+        );
+        result = false;
+        continue;
+      }
+
+      diffBuilder.withPath(key, function() {
+        if (
+          !self.eq_(a[key], b[key], aStack, bStack, customTesters, diffBuilder)
+        ) {
+          result = false;
+        }
+      });
+    }
+
+    if (!result) {
+      return false;
+    }
+
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+
+    return result;
+  };
+
+  function keys(obj, isArray) {
+    var allKeys = Object.keys
+      ? Object.keys(obj)
+      : (function(o) {
+          var keys = [];
+          for (var key in o) {
+            if (j$.util.has(o, key)) {
+              keys.push(key);
+            }
+          }
+          return keys;
+        })(obj);
+
+    if (!isArray) {
+      return allKeys;
+    }
+
+    if (allKeys.length === 0) {
+      return allKeys;
+    }
+
+    var extraKeys = [];
+    for (var i = 0; i < allKeys.length; i++) {
+      if (!/^[0-9]+$/.test(allKeys[i])) {
+        extraKeys.push(allKeys[i]);
+      }
+    }
+
+    return extraKeys;
+  }
+
+  function isFunction(obj) {
+    return typeof obj === 'function';
+  }
+
+  function objectKeysAreDifferentFormatter(pp, actual, expected, path) {
+    var missingProperties = j$.util.objectDifference(expected, actual),
+      extraProperties = j$.util.objectDifference(actual, expected),
+      missingPropertiesMessage = formatKeyValuePairs(pp, missingProperties),
+      extraPropertiesMessage = formatKeyValuePairs(pp, extraProperties),
+      messages = [];
+
+    if (!path.depth()) {
+      path = 'object';
+    }
+
+    if (missingPropertiesMessage.length) {
+      messages.push(
+        'Expected ' + path + ' to have properties' + missingPropertiesMessage
+      );
+    }
+
+    if (extraPropertiesMessage.length) {
+      messages.push(
+        'Expected ' + path + ' not to have properties' + extraPropertiesMessage
+      );
+    }
+
+    return messages.join('\n');
+  }
+
+  function constructorsAreDifferentFormatter(pp, actual, expected, path) {
+    if (!path.depth()) {
+      path = 'object';
+    }
+
+    return (
+      'Expected ' +
+      path +
+      ' to be a kind of ' +
+      j$.fnNameFor(expected.constructor) +
+      ', but was ' +
+      pp(actual) +
+      '.'
+    );
+  }
+
+  function actualArrayIsLongerFormatter(pp, actual, expected, path) {
+    return (
+      'Unexpected ' +
+      path +
+      (path.depth() ? ' = ' : '') +
+      pp(actual) +
+      ' in array.'
+    );
+  }
+
+  function formatKeyValuePairs(pp, obj) {
+    var formatted = '';
+    for (var key in obj) {
+      formatted += '\n    ' + key + ': ' + pp(obj[key]);
+    }
+    return formatted;
+  }
+
+  function isDiffBuilder(obj) {
+    return obj && typeof obj.recordMismatch === 'function';
+  }
+
+  return MatchersUtil;
+};
+
+/**
+ * @interface AsymmetricEqualityTester
+ * @classdesc An asymmetric equality tester is an object that can match multiple
+ * objects. Examples include jasmine.any() and jasmine.stringMatching(). Jasmine
+ * includes a number of built-in asymmetric equality testers, such as
+ * {@link jasmine.objectContaining}. User-defined asymmetric equality testers are
+ * also supported.
+ *
+ * Asymmetric equality testers work with any matcher, including user-defined
+ * custom matchers, that uses {@link MatchersUtil#equals} or
+ * {@link MatchersUtil#contains}.
+ *
+ * @example
+ * function numberDivisibleBy(divisor) {
+ *   return {
+ *     asymmetricMatch: function(n) {
+ *       return typeof n === 'number' && n % divisor === 0;
+ *     },
+ *     jasmineToString: function() {
+ *       return `<a number divisible by ${divisor}>`;
+ *     }
+ *   };
+ * }
+ *
+ * var actual = {
+ *   n: 2,
+ *   otherFields: "don't care"
+ * };
+ *
+ * expect(actual).toEqual(jasmine.objectContaining({n: numberDivisibleBy(2)}));
+ * @see custom_asymmetric_equality_testers
+ * @since 2.0.0
+ */
+/**
+ * Determines whether a value matches this tester
+ * @function
+ * @name AsymmetricEqualityTester#asymmetricMatch
+ * @param value {any} The value to test
+ * @param matchersUtil {MatchersUtil} utilities for testing equality, etc
+ * @return {Boolean}
+ */
+/**
+ * Returns a string representation of this tester to use in matcher failure messages
+ * @function
+ * @name AsymmetricEqualityTester#jasmineToString
+ * @param pp {function} Function that takes a value and returns a pretty-printed representation
+ * @return {String}
+ */
+
+getJasmineRequireObj().MismatchTree = function(j$) {
+  /*
+    To be able to apply custom object formatters at all possible levels of an
+    object graph, DiffBuilder needs to be able to know not just where the
+    mismatch occurred but also all ancestors of the mismatched value in both
+    the expected and actual object graphs. MismatchTree maintains that context
+    and provides it via the traverse method.
+   */
+  function MismatchTree(path) {
+    this.path = path || new j$.ObjectPath([]);
+    this.formatter = undefined;
+    this.children = [];
+    this.isMismatch = false;
+  }
+
+  MismatchTree.prototype.add = function(path, formatter) {
+    var key, child;
+
+    if (path.depth() === 0) {
+      this.formatter = formatter;
+      this.isMismatch = true;
+    } else {
+      key = path.components[0];
+      path = path.shift();
+      child = this.child(key);
+
+      if (!child) {
+        child = new MismatchTree(this.path.add(key));
+        this.children.push(child);
+      }
+
+      child.add(path, formatter);
+    }
+  };
+
+  MismatchTree.prototype.traverse = function(visit) {
+    var i,
+      hasChildren = this.children.length > 0;
+
+    if (this.isMismatch || hasChildren) {
+      if (visit(this.path, !hasChildren, this.formatter)) {
+        for (i = 0; i < this.children.length; i++) {
+          this.children[i].traverse(visit);
+        }
+      }
+    }
+  };
+
+  MismatchTree.prototype.child = function(key) {
+    var i, pathEls;
+
+    for (i = 0; i < this.children.length; i++) {
+      pathEls = this.children[i].path.components;
+      if (pathEls[pathEls.length - 1] === key) {
+        return this.children[i];
+      }
+    }
+  };
+
+  return MismatchTree;
+};
+
+getJasmineRequireObj().nothing = function() {
+  /**
+   * {@link expect} nothing explicitly.
+   * @function
+   * @name matchers#nothing
+   * @since 2.8.0
+   * @example
+   * expect().nothing();
+   */
+  function nothing() {
+    return {
+      compare: function() {
+        return {
+          pass: true
+        };
+      }
+    };
+  }
+
+  return nothing;
+};
+
+getJasmineRequireObj().NullDiffBuilder = function(j$) {
+  return function() {
+    return {
+      withPath: function(_, block) {
+        block();
+      },
+      setRoots: function() {},
+      recordMismatch: function() {}
+    };
+  };
+};
+
+getJasmineRequireObj().ObjectPath = function(j$) {
+  function ObjectPath(components) {
+    this.components = components || [];
+  }
+
+  ObjectPath.prototype.toString = function() {
+    if (this.components.length) {
+      return '$' + map(this.components, formatPropertyAccess).join('');
+    } else {
+      return '';
+    }
+  };
+
+  ObjectPath.prototype.add = function(component) {
+    return new ObjectPath(this.components.concat([component]));
+  };
+
+  ObjectPath.prototype.shift = function() {
+    return new ObjectPath(this.components.slice(1));
+  };
+
+  ObjectPath.prototype.depth = function() {
+    return this.components.length;
+  };
+
+  function formatPropertyAccess(prop) {
+    if (typeof prop === 'number') {
+      return '[' + prop + ']';
+    }
+
+    if (isValidIdentifier(prop)) {
+      return '.' + prop;
+    }
+
+    return "['" + prop + "']";
+  }
+
+  function map(array, fn) {
+    var results = [];
+    for (var i = 0; i < array.length; i++) {
+      results.push(fn(array[i]));
+    }
+    return results;
+  }
+
+  function isValidIdentifier(string) {
+    return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string);
+  }
+
+  return ObjectPath;
+};
+
+getJasmineRequireObj().requireAsyncMatchers = function(jRequire, j$) {
+  var availableMatchers = [
+      'toBePending',
+      'toBeResolved',
+      'toBeRejected',
+      'toBeResolvedTo',
+      'toBeRejectedWith',
+      'toBeRejectedWithError'
+    ],
+    matchers = {};
+
+  for (var i = 0; i < availableMatchers.length; i++) {
+    var name = availableMatchers[i];
+    matchers[name] = jRequire[name](j$);
+  }
+
+  return matchers;
+};
+
+getJasmineRequireObj().toBe = function(j$) {
+  /**
+   * {@link expect} the actual value to be `===` to the expected value.
+   * @function
+   * @name matchers#toBe
+   * @since 1.3.0
+   * @param {Object} expected - The expected value to compare against.
+   * @example
+   * expect(thing).toBe(realThing);
+   */
+  function toBe(matchersUtil) {
+    var tip =
+      ' Tip: To check for deep equality, use .toEqual() instead of .toBe().';
+
+    return {
+      compare: function(actual, expected) {
+        var result = {
+          pass: actual === expected
+        };
+
+        if (typeof expected === 'object') {
+          result.message =
+            matchersUtil.buildFailureMessage(
+              'toBe',
+              result.pass,
+              actual,
+              expected
+            ) + tip;
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toBe;
+};
+
+getJasmineRequireObj().toBeCloseTo = function() {
+  /**
+   * {@link expect} the actual value to be within a specified precision of the expected value.
+   * @function
+   * @name matchers#toBeCloseTo
+   * @since 1.3.0
+   * @param {Object} expected - The expected value to compare against.
+   * @param {Number} [precision=2] - The number of decimal points to check.
+   * @example
+   * expect(number).toBeCloseTo(42.2, 3);
+   */
+  function toBeCloseTo() {
+    return {
+      compare: function(actual, expected, precision) {
+        if (precision !== 0) {
+          precision = precision || 2;
+        }
+
+        if (expected === null || actual === null) {
+          throw new Error(
+            'Cannot use toBeCloseTo with null. Arguments evaluated to: ' +
+              'expect(' +
+              actual +
+              ').toBeCloseTo(' +
+              expected +
+              ').'
+          );
+        }
+
+        var pow = Math.pow(10, precision + 1);
+        var delta = Math.abs(expected - actual);
+        var maxDelta = Math.pow(10, -precision) / 2;
+
+        return {
+          pass: Math.round(delta * pow) <= maxDelta * pow
+        };
+      }
+    };
+  }
+
+  return toBeCloseTo;
+};
+
+getJasmineRequireObj().toBeDefined = function() {
+  /**
+   * {@link expect} the actual value to be defined. (Not `undefined`)
+   * @function
+   * @name matchers#toBeDefined
+   * @since 1.3.0
+   * @example
+   * expect(result).toBeDefined();
+   */
+  function toBeDefined() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: void 0 !== actual
+        };
+      }
+    };
+  }
+
+  return toBeDefined;
+};
+
+getJasmineRequireObj().toBeFalse = function() {
+  /**
+   * {@link expect} the actual value to be `false`.
+   * @function
+   * @name matchers#toBeFalse
+   * @since 3.5.0
+   * @example
+   * expect(result).toBeFalse();
+   */
+  function toBeFalse() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: actual === false
+        };
+      }
+    };
+  }
+
+  return toBeFalse;
+};
+
+getJasmineRequireObj().toBeFalsy = function() {
+  /**
+   * {@link expect} the actual value to be falsy
+   * @function
+   * @name matchers#toBeFalsy
+   * @since 2.0.0
+   * @example
+   * expect(result).toBeFalsy();
+   */
+  function toBeFalsy() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: !actual
+        };
+      }
+    };
+  }
+
+  return toBeFalsy;
+};
+
+getJasmineRequireObj().toBeGreaterThan = function() {
+  /**
+   * {@link expect} the actual value to be greater than the expected value.
+   * @function
+   * @name matchers#toBeGreaterThan
+   * @since 2.0.0
+   * @param {Number} expected - The value to compare against.
+   * @example
+   * expect(result).toBeGreaterThan(3);
+   */
+  function toBeGreaterThan() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual > expected
+        };
+      }
+    };
+  }
+
+  return toBeGreaterThan;
+};
+
+getJasmineRequireObj().toBeGreaterThanOrEqual = function() {
+  /**
+   * {@link expect} the actual value to be greater than or equal to the expected value.
+   * @function
+   * @name matchers#toBeGreaterThanOrEqual
+   * @since 2.0.0
+   * @param {Number} expected - The expected value to compare against.
+   * @example
+   * expect(result).toBeGreaterThanOrEqual(25);
+   */
+  function toBeGreaterThanOrEqual() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual >= expected
+        };
+      }
+    };
+  }
+
+  return toBeGreaterThanOrEqual;
+};
+
+getJasmineRequireObj().toBeInstanceOf = function(j$) {
+  var usageError = j$.formatErrorMsg(
+    '<toBeInstanceOf>',
+    'expect(value).toBeInstanceOf(<ConstructorFunction>)'
+  );
+
+  /**
+   * {@link expect} the actual to be an instance of the expected class
+   * @function
+   * @name matchers#toBeInstanceOf
+   * @since 3.5.0
+   * @param {Object} expected - The class or constructor function to check for
+   * @example
+   * expect('foo').toBeInstanceOf(String);
+   * expect(3).toBeInstanceOf(Number);
+   * expect(new Error()).toBeInstanceOf(Error);
+   */
+  function toBeInstanceOf(matchersUtil) {
+    return {
+      compare: function(actual, expected) {
+        var actualType =
+            actual && actual.constructor
+              ? j$.fnNameFor(actual.constructor)
+              : matchersUtil.pp(actual),
+          expectedType = expected
+            ? j$.fnNameFor(expected)
+            : matchersUtil.pp(expected),
+          expectedMatcher,
+          pass;
+
+        try {
+          expectedMatcher = new j$.Any(expected);
+          pass = expectedMatcher.asymmetricMatch(actual);
+        } catch (error) {
+          throw new Error(
+            usageError('Expected value is not a constructor function')
+          );
+        }
+
+        if (pass) {
+          return {
+            pass: true,
+            message:
+              'Expected instance of ' +
+              actualType +
+              ' not to be an instance of ' +
+              expectedType
+          };
+        } else {
+          return {
+            pass: false,
+            message:
+              'Expected instance of ' +
+              actualType +
+              ' to be an instance of ' +
+              expectedType
+          };
+        }
+      }
+    };
+  }
+
+  return toBeInstanceOf;
+};
+
+getJasmineRequireObj().toBeLessThan = function() {
+  /**
+   * {@link expect} the actual value to be less than the expected value.
+   * @function
+   * @name matchers#toBeLessThan
+   * @since 2.0.0
+   * @param {Number} expected - The expected value to compare against.
+   * @example
+   * expect(result).toBeLessThan(0);
+   */
+  function toBeLessThan() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual < expected
+        };
+      }
+    };
+  }
+
+  return toBeLessThan;
+};
+
+getJasmineRequireObj().toBeLessThanOrEqual = function() {
+  /**
+   * {@link expect} the actual value to be less than or equal to the expected value.
+   * @function
+   * @name matchers#toBeLessThanOrEqual
+   * @since 2.0.0
+   * @param {Number} expected - The expected value to compare against.
+   * @example
+   * expect(result).toBeLessThanOrEqual(123);
+   */
+  function toBeLessThanOrEqual() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual <= expected
+        };
+      }
+    };
+  }
+
+  return toBeLessThanOrEqual;
+};
+
+getJasmineRequireObj().toBeNaN = function(j$) {
+  /**
+   * {@link expect} the actual value to be `NaN` (Not a Number).
+   * @function
+   * @name matchers#toBeNaN
+   * @since 1.3.0
+   * @example
+   * expect(thing).toBeNaN();
+   */
+  function toBeNaN(matchersUtil) {
+    return {
+      compare: function(actual) {
+        var result = {
+          pass: actual !== actual
+        };
+
+        if (result.pass) {
+          result.message = 'Expected actual not to be NaN.';
+        } else {
+          result.message = function() {
+            return 'Expected ' + matchersUtil.pp(actual) + ' to be NaN.';
+          };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toBeNaN;
+};
+
+getJasmineRequireObj().toBeNegativeInfinity = function(j$) {
+  /**
+   * {@link expect} the actual value to be `-Infinity` (-infinity).
+   * @function
+   * @name matchers#toBeNegativeInfinity
+   * @since 2.6.0
+   * @example
+   * expect(thing).toBeNegativeInfinity();
+   */
+  function toBeNegativeInfinity(matchersUtil) {
+    return {
+      compare: function(actual) {
+        var result = {
+          pass: actual === Number.NEGATIVE_INFINITY
+        };
+
+        if (result.pass) {
+          result.message = 'Expected actual not to be -Infinity.';
+        } else {
+          result.message = function() {
+            return 'Expected ' + matchersUtil.pp(actual) + ' to be -Infinity.';
+          };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toBeNegativeInfinity;
+};
+
+getJasmineRequireObj().toBeNull = function() {
+  /**
+   * {@link expect} the actual value to be `null`.
+   * @function
+   * @name matchers#toBeNull
+   * @since 1.3.0
+   * @example
+   * expect(result).toBeNull();
+   */
+  function toBeNull() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: actual === null
+        };
+      }
+    };
+  }
+
+  return toBeNull;
+};
+
+getJasmineRequireObj().toBePositiveInfinity = function(j$) {
+  /**
+   * {@link expect} the actual value to be `Infinity` (infinity).
+   * @function
+   * @name matchers#toBePositiveInfinity
+   * @since 2.6.0
+   * @example
+   * expect(thing).toBePositiveInfinity();
+   */
+  function toBePositiveInfinity(matchersUtil) {
+    return {
+      compare: function(actual) {
+        var result = {
+          pass: actual === Number.POSITIVE_INFINITY
+        };
+
+        if (result.pass) {
+          result.message = 'Expected actual not to be Infinity.';
+        } else {
+          result.message = function() {
+            return 'Expected ' + matchersUtil.pp(actual) + ' to be Infinity.';
+          };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toBePositiveInfinity;
+};
+
+getJasmineRequireObj().toBeTrue = function() {
+  /**
+   * {@link expect} the actual value to be `true`.
+   * @function
+   * @name matchers#toBeTrue
+   * @since 3.5.0
+   * @example
+   * expect(result).toBeTrue();
+   */
+  function toBeTrue() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: actual === true
+        };
+      }
+    };
+  }
+
+  return toBeTrue;
+};
+
+getJasmineRequireObj().toBeTruthy = function() {
+  /**
+   * {@link expect} the actual value to be truthy.
+   * @function
+   * @name matchers#toBeTruthy
+   * @since 2.0.0
+   * @example
+   * expect(thing).toBeTruthy();
+   */
+  function toBeTruthy() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: !!actual
+        };
+      }
+    };
+  }
+
+  return toBeTruthy;
+};
+
+getJasmineRequireObj().toBeUndefined = function() {
+  /**
+   * {@link expect} the actual value to be `undefined`.
+   * @function
+   * @name matchers#toBeUndefined
+   * @since 1.3.0
+   * @example
+   * expect(result).toBeUndefined():
+   */
+  function toBeUndefined() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: void 0 === actual
+        };
+      }
+    };
+  }
+
+  return toBeUndefined;
+};
+
+getJasmineRequireObj().toContain = function() {
+  /**
+   * {@link expect} the actual value to contain a specific value.
+   * @function
+   * @name matchers#toContain
+   * @since 2.0.0
+   * @param {Object} expected - The value to look for.
+   * @example
+   * expect(array).toContain(anElement);
+   * expect(string).toContain(substring);
+   */
+  function toContain(matchersUtil) {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: matchersUtil.contains(actual, expected)
+        };
+      }
+    };
+  }
+
+  return toContain;
+};
+
+getJasmineRequireObj().toEqual = function(j$) {
+  /**
+   * {@link expect} the actual value to be equal to the expected, using deep equality comparison.
+   * @function
+   * @name matchers#toEqual
+   * @since 1.3.0
+   * @param {Object} expected - Expected value
+   * @example
+   * expect(bigObject).toEqual({"foo": ['bar', 'baz']});
+   */
+  function toEqual(matchersUtil) {
+    return {
+      compare: function(actual, expected) {
+        var result = {
+            pass: false
+          },
+          diffBuilder = j$.DiffBuilder({ prettyPrinter: matchersUtil.pp });
+
+        result.pass = matchersUtil.equals(actual, expected, diffBuilder);
+
+        // TODO: only set error message if test fails
+        result.message = diffBuilder.getMessage();
+
+        return result;
+      }
+    };
+  }
+
+  return toEqual;
+};
+
+getJasmineRequireObj().toHaveBeenCalled = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toHaveBeenCalled>',
+    'expect(<spyObj>).toHaveBeenCalled()'
+  );
+
+  /**
+   * {@link expect} the actual (a {@link Spy}) to have been called.
+   * @function
+   * @name matchers#toHaveBeenCalled
+   * @since 1.3.0
+   * @example
+   * expect(mySpy).toHaveBeenCalled();
+   * expect(mySpy).not.toHaveBeenCalled();
+   */
+  function toHaveBeenCalled(matchersUtil) {
+    return {
+      compare: function(actual) {
+        var result = {};
+
+        if (!j$.isSpy(actual)) {
+          throw new Error(
+            getErrorMsg(
+              'Expected a spy, but got ' + matchersUtil.pp(actual) + '.'
+            )
+          );
+        }
+
+        if (arguments.length > 1) {
+          throw new Error(
+            getErrorMsg('Does not take arguments, use toHaveBeenCalledWith')
+          );
+        }
+
+        result.pass = actual.calls.any();
+
+        result.message = result.pass
+          ? 'Expected spy ' + actual.and.identity + ' not to have been called.'
+          : 'Expected spy ' + actual.and.identity + ' to have been called.';
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalled;
+};
+
+getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toHaveBeenCalledBefore>',
+    'expect(<spyObj>).toHaveBeenCalledBefore(<spyObj>)'
+  );
+
+  /**
+   * {@link expect} the actual value (a {@link Spy}) to have been called before another {@link Spy}.
+   * @function
+   * @name matchers#toHaveBeenCalledBefore
+   * @since 2.6.0
+   * @param {Spy} expected - {@link Spy} that should have been called after the `actual` {@link Spy}.
+   * @example
+   * expect(mySpy).toHaveBeenCalledBefore(otherSpy);
+   */
+  function toHaveBeenCalledBefore(matchersUtil) {
+    return {
+      compare: function(firstSpy, latterSpy) {
+        if (!j$.isSpy(firstSpy)) {
+          throw new Error(
+            getErrorMsg(
+              'Expected a spy, but got ' + matchersUtil.pp(firstSpy) + '.'
+            )
+          );
+        }
+        if (!j$.isSpy(latterSpy)) {
+          throw new Error(
+            getErrorMsg(
+              'Expected a spy, but got ' + matchersUtil.pp(latterSpy) + '.'
+            )
+          );
+        }
+
+        var result = { pass: false };
+
+        if (!firstSpy.calls.count()) {
+          result.message =
+            'Expected spy ' + firstSpy.and.identity + ' to have been called.';
+          return result;
+        }
+        if (!latterSpy.calls.count()) {
+          result.message =
+            'Expected spy ' + latterSpy.and.identity + ' to have been called.';
+          return result;
+        }
+
+        var latest1stSpyCall = firstSpy.calls.mostRecent().invocationOrder;
+        var first2ndSpyCall = latterSpy.calls.first().invocationOrder;
+
+        result.pass = latest1stSpyCall < first2ndSpyCall;
+
+        if (result.pass) {
+          result.message =
+            'Expected spy ' +
+            firstSpy.and.identity +
+            ' to not have been called before spy ' +
+            latterSpy.and.identity +
+            ', but it was';
+        } else {
+          var first1stSpyCall = firstSpy.calls.first().invocationOrder;
+          var latest2ndSpyCall = latterSpy.calls.mostRecent().invocationOrder;
+
+          if (first1stSpyCall < first2ndSpyCall) {
+            result.message =
+              'Expected latest call to spy ' +
+              firstSpy.and.identity +
+              ' to have been called before first call to spy ' +
+              latterSpy.and.identity +
+              ' (no interleaved calls)';
+          } else if (latest2ndSpyCall > latest1stSpyCall) {
+            result.message =
+              'Expected first call to spy ' +
+              latterSpy.and.identity +
+              ' to have been called after latest call to spy ' +
+              firstSpy.and.identity +
+              ' (no interleaved calls)';
+          } else {
+            result.message =
+              'Expected spy ' +
+              firstSpy.and.identity +
+              ' to have been called before spy ' +
+              latterSpy.and.identity;
+          }
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalledBefore;
+};
+
+getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toHaveBeenCalledOnceWith>',
+    'expect(<spyObj>).toHaveBeenCalledOnceWith(...arguments)'
+  );
+
+  /**
+   * {@link expect} the actual (a {@link Spy}) to have been called exactly once, and exactly with the particular arguments.
+   * @function
+   * @name matchers#toHaveBeenCalledOnceWith
+   * @since 3.6.0
+   * @param {...Object} - The arguments to look for
+   * @example
+   * expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2);
+   */
+  function toHaveBeenCalledOnceWith(util) {
+    return {
+      compare: function() {
+        var args = Array.prototype.slice.call(arguments, 0),
+          actual = args[0],
+          expectedArgs = args.slice(1);
+
+        if (!j$.isSpy(actual)) {
+          throw new Error(
+            getErrorMsg('Expected a spy, but got ' + util.pp(actual) + '.')
+          );
+        }
+
+        var prettyPrintedCalls = actual.calls
+          .allArgs()
+          .map(function(argsForCall) {
+            return '  ' + util.pp(argsForCall);
+          });
+
+        if (
+          actual.calls.count() === 1 &&
+          util.contains(actual.calls.allArgs(), expectedArgs)
+        ) {
+          return {
+            pass: true,
+            message:
+              'Expected spy ' +
+              actual.and.identity +
+              ' to have been called 0 times, multiple times, or once, but with arguments different from:\n' +
+              '  ' +
+              util.pp(expectedArgs) +
+              '\n' +
+              'But the actual call was:\n' +
+              prettyPrintedCalls.join(',\n') +
+              '.\n\n'
+          };
+        }
+
+        function getDiffs() {
+          return actual.calls.allArgs().map(function(argsForCall, callIx) {
+            var diffBuilder = new j$.DiffBuilder();
+            util.equals(argsForCall, expectedArgs, diffBuilder);
+            return diffBuilder.getMessage();
+          });
+        }
+
+        function butString() {
+          switch (actual.calls.count()) {
+            case 0:
+              return 'But it was never called.\n\n';
+            case 1:
+              return (
+                'But the actual call was:\n' +
+                prettyPrintedCalls.join(',\n') +
+                '.\n' +
+                getDiffs().join('\n') +
+                '\n\n'
+              );
+            default:
+              return (
+                'But the actual calls were:\n' +
+                prettyPrintedCalls.join(',\n') +
+                '.\n\n'
+              );
+          }
+        }
+
+        return {
+          pass: false,
+          message:
+            'Expected spy ' +
+            actual.and.identity +
+            ' to have been called only once, and with given args:\n' +
+            '  ' +
+            util.pp(expectedArgs) +
+            '\n' +
+            butString()
+        };
+      }
+    };
+  }
+
+  return toHaveBeenCalledOnceWith;
+};
+
+getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toHaveBeenCalledTimes>',
+    'expect(<spyObj>).toHaveBeenCalledTimes(<Number>)'
+  );
+
+  /**
+   * {@link expect} the actual (a {@link Spy}) to have been called the specified number of times.
+   * @function
+   * @name matchers#toHaveBeenCalledTimes
+   * @since 2.4.0
+   * @param {Number} expected - The number of invocations to look for.
+   * @example
+   * expect(mySpy).toHaveBeenCalledTimes(3);
+   */
+  function toHaveBeenCalledTimes(matchersUtil) {
+    return {
+      compare: function(actual, expected) {
+        if (!j$.isSpy(actual)) {
+          throw new Error(
+            getErrorMsg(
+              'Expected a spy, but got ' + matchersUtil.pp(actual) + '.'
+            )
+          );
+        }
+
+        var args = Array.prototype.slice.call(arguments, 0),
+          result = { pass: false };
+
+        if (!j$.isNumber_(expected)) {
+          throw new Error(
+            getErrorMsg(
+              'The expected times failed is a required argument and must be a number.'
+            )
+          );
+        }
+
+        actual = args[0];
+        var calls = actual.calls.count();
+        var timesMessage = expected === 1 ? 'once' : expected + ' times';
+        result.pass = calls === expected;
+        result.message = result.pass
+          ? 'Expected spy ' +
+            actual.and.identity +
+            ' not to have been called ' +
+            timesMessage +
+            '. It was called ' +
+            calls +
+            ' times.'
+          : 'Expected spy ' +
+            actual.and.identity +
+            ' to have been called ' +
+            timesMessage +
+            '. It was called ' +
+            calls +
+            ' times.';
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalledTimes;
+};
+
+getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toHaveBeenCalledWith>',
+    'expect(<spyObj>).toHaveBeenCalledWith(...arguments)'
+  );
+
+  /**
+   * {@link expect} the actual (a {@link Spy}) to have been called with particular arguments at least once.
+   * @function
+   * @name matchers#toHaveBeenCalledWith
+   * @since 1.3.0
+   * @param {...Object} - The arguments to look for
+   * @example
+   * expect(mySpy).toHaveBeenCalledWith('foo', 'bar', 2);
+   */
+  function toHaveBeenCalledWith(matchersUtil) {
+    return {
+      compare: function() {
+        var args = Array.prototype.slice.call(arguments, 0),
+          actual = args[0],
+          expectedArgs = args.slice(1),
+          result = { pass: false };
+
+        if (!j$.isSpy(actual)) {
+          throw new Error(
+            getErrorMsg(
+              'Expected a spy, but got ' + matchersUtil.pp(actual) + '.'
+            )
+          );
+        }
+
+        if (!actual.calls.any()) {
+          result.message = function() {
+            return (
+              'Expected spy ' +
+              actual.and.identity +
+              ' to have been called with:\n' +
+              '  ' +
+              matchersUtil.pp(expectedArgs) +
+              '\nbut it was never called.'
+            );
+          };
+          return result;
+        }
+
+        if (matchersUtil.contains(actual.calls.allArgs(), expectedArgs)) {
+          result.pass = true;
+          result.message = function() {
+            return (
+              'Expected spy ' +
+              actual.and.identity +
+              ' not to have been called with:\n' +
+              '  ' +
+              matchersUtil.pp(expectedArgs) +
+              '\nbut it was.'
+            );
+          };
+        } else {
+          result.message = function() {
+            var prettyPrintedCalls = actual.calls
+              .allArgs()
+              .map(function(argsForCall) {
+                return '  ' + matchersUtil.pp(argsForCall);
+              });
+
+            var diffs = actual.calls
+              .allArgs()
+              .map(function(argsForCall, callIx) {
+                var diffBuilder = new j$.DiffBuilder();
+                matchersUtil.equals(argsForCall, expectedArgs, diffBuilder);
+                return (
+                  'Call ' +
+                  callIx +
+                  ':\n' +
+                  diffBuilder.getMessage().replace(/^/gm, '  ')
+                );
+              });
+
+            return (
+              'Expected spy ' +
+              actual.and.identity +
+              ' to have been called with:\n' +
+              '  ' +
+              matchersUtil.pp(expectedArgs) +
+              '\n' +
+              '' +
+              'but actual calls were:\n' +
+              prettyPrintedCalls.join(',\n') +
+              '.\n\n' +
+              diffs.join('\n')
+            );
+          };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalledWith;
+};
+
+getJasmineRequireObj().toHaveClass = function(j$) {
+  /**
+   * {@link expect} the actual value to be a DOM element that has the expected class
+   * @function
+   * @name matchers#toHaveClass
+   * @since 3.0.0
+   * @param {Object} expected - The class name to test for
+   * @example
+   * var el = document.createElement('div');
+   * el.className = 'foo bar baz';
+   * expect(el).toHaveClass('bar');
+   */
+  function toHaveClass(matchersUtil) {
+    return {
+      compare: function(actual, expected) {
+        if (!isElement(actual)) {
+          throw new Error(matchersUtil.pp(actual) + ' is not a DOM element');
+        }
+
+        return {
+          pass: actual.classList.contains(expected)
+        };
+      }
+    };
+  }
+
+  function isElement(maybeEl) {
+    return (
+      maybeEl && maybeEl.classList && j$.isFunction_(maybeEl.classList.contains)
+    );
+  }
+
+  return toHaveClass;
+};
+
+getJasmineRequireObj().toHaveSize = function(j$) {
+  /**
+   * {@link expect} the actual size to be equal to the expected, using array-like length or object keys size.
+   * @function
+   * @name matchers#toHaveSize
+   * @since 3.6.0
+   * @param {Object} expected - Expected size
+   * @example
+   * array = [1,2];
+   * expect(array).toHaveSize(2);
+   */
+  function toHaveSize() {
+    return {
+      compare: function(actual, expected) {
+        var result = {
+          pass: false
+        };
+
+        if (
+          j$.isA_('WeakSet', actual) ||
+          j$.isWeakMap(actual) ||
+          j$.isDataView(actual)
+        ) {
+          throw new Error('Cannot get size of ' + actual + '.');
+        }
+
+        if (j$.isSet(actual) || j$.isMap(actual)) {
+          result.pass = actual.size === expected;
+        } else if (isLength(actual.length)) {
+          result.pass = actual.length === expected;
+        } else {
+          result.pass = Object.keys(actual).length === expected;
+        }
+
+        return result;
+      }
+    };
+  }
+
+  var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // eslint-disable-line compat/compat
+  function isLength(value) {
+    return (
+      typeof value == 'number' &&
+      value > -1 &&
+      value % 1 === 0 &&
+      value <= MAX_SAFE_INTEGER
+    );
+  }
+
+  return toHaveSize;
+};
+
+getJasmineRequireObj().toMatch = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toMatch>',
+    'expect(<expectation>).toMatch(<string> || <regexp>)'
+  );
+
+  /**
+   * {@link expect} the actual value to match a regular expression
+   * @function
+   * @name matchers#toMatch
+   * @since 1.3.0
+   * @param {RegExp|String} expected - Value to look for in the string.
+   * @example
+   * expect("my string").toMatch(/string$/);
+   * expect("other string").toMatch("her");
+   */
+  function toMatch() {
+    return {
+      compare: function(actual, expected) {
+        if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) {
+          throw new Error(getErrorMsg('Expected is not a String or a RegExp'));
+        }
+
+        var regexp = new RegExp(expected);
+
+        return {
+          pass: regexp.test(actual)
+        };
+      }
+    };
+  }
+
+  return toMatch;
+};
+
+getJasmineRequireObj().toThrow = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toThrow>',
+    'expect(function() {<expectation>}).toThrow()'
+  );
+
+  /**
+   * {@link expect} a function to `throw` something.
+   * @function
+   * @name matchers#toThrow
+   * @since 2.0.0
+   * @param {Object} [expected] - Value that should be thrown. If not provided, simply the fact that something was thrown will be checked.
+   * @example
+   * expect(function() { return 'things'; }).toThrow('foo');
+   * expect(function() { return 'stuff'; }).toThrow();
+   */
+  function toThrow(matchersUtil) {
+    return {
+      compare: function(actual, expected) {
+        var result = { pass: false },
+          threw = false,
+          thrown;
+
+        if (typeof actual != 'function') {
+          throw new Error(getErrorMsg('Actual is not a Function'));
+        }
+
+        try {
+          actual();
+        } catch (e) {
+          threw = true;
+          thrown = e;
+        }
+
+        if (!threw) {
+          result.message = 'Expected function to throw an exception.';
+          return result;
+        }
+
+        if (arguments.length == 1) {
+          result.pass = true;
+          result.message = function() {
+            return (
+              'Expected function not to throw, but it threw ' +
+              matchersUtil.pp(thrown) +
+              '.'
+            );
+          };
+
+          return result;
+        }
+
+        if (matchersUtil.equals(thrown, expected)) {
+          result.pass = true;
+          result.message = function() {
+            return (
+              'Expected function not to throw ' +
+              matchersUtil.pp(expected) +
+              '.'
+            );
+          };
+        } else {
+          result.message = function() {
+            return (
+              'Expected function to throw ' +
+              matchersUtil.pp(expected) +
+              ', but it threw ' +
+              matchersUtil.pp(thrown) +
+              '.'
+            );
+          };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toThrow;
+};
+
+getJasmineRequireObj().toThrowError = function(j$) {
+  var getErrorMsg = j$.formatErrorMsg(
+    '<toThrowError>',
+    'expect(function() {<expectation>}).toThrowError(<ErrorConstructor>, <message>)'
+  );
+
+  /**
+   * {@link expect} a function to `throw` an `Error`.
+   * @function
+   * @name matchers#toThrowError
+   * @since 2.0.0
+   * @param {Error} [expected] - `Error` constructor the object that was thrown needs to be an instance of. If not provided, `Error` will be used.
+   * @param {RegExp|String} [message] - The message that should be set on the thrown `Error`
+   * @example
+   * expect(function() { return 'things'; }).toThrowError(MyCustomError, 'message');
+   * expect(function() { return 'things'; }).toThrowError(MyCustomError, /bar/);
+   * expect(function() { return 'stuff'; }).toThrowError(MyCustomError);
+   * expect(function() { return 'other'; }).toThrowError(/foo/);
+   * expect(function() { return 'other'; }).toThrowError();
+   */
+  function toThrowError(matchersUtil) {
+    return {
+      compare: function(actual) {
+        var errorMatcher = getMatcher.apply(null, arguments),
+          thrown;
+
+        if (typeof actual != 'function') {
+          throw new Error(getErrorMsg('Actual is not a Function'));
+        }
+
+        try {
+          actual();
+          return fail('Expected function to throw an Error.');
+        } catch (e) {
+          thrown = e;
+        }
+
+        if (!j$.isError_(thrown)) {
+          return fail(function() {
+            return (
+              'Expected function to throw an Error, but it threw ' +
+              matchersUtil.pp(thrown) +
+              '.'
+            );
+          });
+        }
+
+        return errorMatcher.match(thrown);
+      }
+    };
+
+    function getMatcher() {
+      var expected, errorType;
+
+      if (arguments[2]) {
+        errorType = arguments[1];
+        expected = arguments[2];
+        if (!isAnErrorType(errorType)) {
+          throw new Error(getErrorMsg('Expected error type is not an Error.'));
+        }
+
+        return exactMatcher(expected, errorType);
+      } else if (arguments[1]) {
+        expected = arguments[1];
+
+        if (isAnErrorType(arguments[1])) {
+          return exactMatcher(null, arguments[1]);
+        } else {
+          return exactMatcher(arguments[1], null);
+        }
+      } else {
+        return anyMatcher();
+      }
+    }
+
+    function anyMatcher() {
+      return {
+        match: function(error) {
+          return pass(
+            'Expected function not to throw an Error, but it threw ' +
+              j$.fnNameFor(error) +
+              '.'
+          );
+        }
+      };
+    }
+
+    function exactMatcher(expected, errorType) {
+      if (expected && !isStringOrRegExp(expected)) {
+        if (errorType) {
+          throw new Error(
+            getErrorMsg('Expected error message is not a string or RegExp.')
+          );
+        } else {
+          throw new Error(
+            getErrorMsg('Expected is not an Error, string, or RegExp.')
+          );
+        }
+      }
+
+      function messageMatch(message) {
+        if (typeof expected == 'string') {
+          return expected == message;
+        } else {
+          return expected.test(message);
+        }
+      }
+
+      var errorTypeDescription = errorType
+        ? j$.fnNameFor(errorType)
+        : 'an exception';
+
+      function thrownDescription(thrown) {
+        var thrownName = errorType
+            ? j$.fnNameFor(thrown.constructor)
+            : 'an exception',
+          thrownMessage = '';
+
+        if (expected) {
+          thrownMessage = ' with message ' + matchersUtil.pp(thrown.message);
+        }
+
+        return thrownName + thrownMessage;
+      }
+
+      function messageDescription() {
+        if (expected === null) {
+          return '';
+        } else if (expected instanceof RegExp) {
+          return ' with a message matching ' + matchersUtil.pp(expected);
+        } else {
+          return ' with message ' + matchersUtil.pp(expected);
+        }
+      }
+
+      function matches(error) {
+        return (
+          (errorType === null || error instanceof errorType) &&
+          (expected === null || messageMatch(error.message))
+        );
+      }
+
+      return {
+        match: function(thrown) {
+          if (matches(thrown)) {
+            return pass(function() {
+              return (
+                'Expected function not to throw ' +
+                errorTypeDescription +
+                messageDescription() +
+                '.'
+              );
+            });
+          } else {
+            return fail(function() {
+              return (
+                'Expected function to throw ' +
+                errorTypeDescription +
+                messageDescription() +
+                ', but it threw ' +
+                thrownDescription(thrown) +
+                '.'
+              );
+            });
+          }
+        }
+      };
+    }
+
+    function isStringOrRegExp(potential) {
+      return potential instanceof RegExp || typeof potential == 'string';
+    }
+
+    function isAnErrorType(type) {
+      if (typeof type !== 'function') {
+        return false;
+      }
+
+      var Surrogate = function() {};
+      Surrogate.prototype = type.prototype;
+      return j$.isError_(new Surrogate());
+    }
+  }
+
+  function pass(message) {
+    return {
+      pass: true,
+      message: message
+    };
+  }
+
+  function fail(message) {
+    return {
+      pass: false,
+      message: message
+    };
+  }
+
+  return toThrowError;
+};
+
+getJasmineRequireObj().toThrowMatching = function(j$) {
+  var usageError = j$.formatErrorMsg(
+    '<toThrowMatching>',
+    'expect(function() {<expectation>}).toThrowMatching(<Predicate>)'
+  );
+
+  /**
+   * {@link expect} a function to `throw` something matching a predicate.
+   * @function
+   * @name matchers#toThrowMatching
+   * @since 3.0.0
+   * @param {Function} predicate - A function that takes the thrown exception as its parameter and returns true if it matches.
+   * @example
+   * expect(function() { throw new Error('nope'); }).toThrowMatching(function(thrown) { return thrown.message === 'nope'; });
+   */
+  function toThrowMatching(matchersUtil) {
+    return {
+      compare: function(actual, predicate) {
+        var thrown;
+
+        if (typeof actual !== 'function') {
+          throw new Error(usageError('Actual is not a Function'));
+        }
+
+        if (typeof predicate !== 'function') {
+          throw new Error(usageError('Predicate is not a Function'));
+        }
+
+        try {
+          actual();
+          return fail('Expected function to throw an exception.');
+        } catch (e) {
+          thrown = e;
+        }
+
+        if (predicate(thrown)) {
+          return pass(
+            'Expected function not to throw an exception matching a predicate.'
+          );
+        } else {
+          return fail(function() {
+            return (
+              'Expected function to throw an exception matching a predicate, ' +
+              'but it threw ' +
+              thrownDescription(thrown) +
+              '.'
+            );
+          });
+        }
+      }
+    };
+
+    function thrownDescription(thrown) {
+      if (thrown && thrown.constructor) {
+        return (
+          j$.fnNameFor(thrown.constructor) +
+          ' with message ' +
+          matchersUtil.pp(thrown.message)
+        );
+      } else {
+        return matchersUtil.pp(thrown);
+      }
+    }
+  }
+
+  function pass(message) {
+    return {
+      pass: true,
+      message: message
+    };
+  }
+
+  function fail(message) {
+    return {
+      pass: false,
+      message: message
+    };
+  }
+
+  return toThrowMatching;
+};
+
+getJasmineRequireObj().MockDate = function(j$) {
+  function MockDate(global) {
+    var self = this;
+    var currentTime = 0;
+
+    if (!global || !global.Date) {
+      self.install = function() {};
+      self.tick = function() {};
+      self.uninstall = function() {};
+      return self;
+    }
+
+    var GlobalDate = global.Date;
+
+    self.install = function(mockDate) {
+      if (mockDate instanceof GlobalDate) {
+        currentTime = mockDate.getTime();
+      } else {
+        if (!j$.util.isUndefined(mockDate)) {
+          j$.getEnv().deprecated(
+            'The argument to jasmine.clock().mockDate(), if specified, ' +
+              'should be a Date instance. Passing anything other than a Date ' +
+              'will be treated as an error in a future release.'
+          );
+        }
+
+        currentTime = new GlobalDate().getTime();
+      }
+
+      global.Date = FakeDate;
+    };
+
+    self.tick = function(millis) {
+      millis = millis || 0;
+      currentTime = currentTime + millis;
+    };
+
+    self.uninstall = function() {
+      currentTime = 0;
+      global.Date = GlobalDate;
+    };
+
+    createDateProperties();
+
+    return self;
+
+    function FakeDate() {
+      switch (arguments.length) {
+        case 0:
+          return new GlobalDate(currentTime);
+        case 1:
+          return new GlobalDate(arguments[0]);
+        case 2:
+          return new GlobalDate(arguments[0], arguments[1]);
+        case 3:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2]);
+        case 4:
+          return new GlobalDate(
+            arguments[0],
+            arguments[1],
+            arguments[2],
+            arguments[3]
+          );
+        case 5:
+          return new GlobalDate(
+            arguments[0],
+            arguments[1],
+            arguments[2],
+            arguments[3],
+            arguments[4]
+          );
+        case 6:
+          return new GlobalDate(
+            arguments[0],
+            arguments[1],
+            arguments[2],
+            arguments[3],
+            arguments[4],
+            arguments[5]
+          );
+        default:
+          return new GlobalDate(
+            arguments[0],
+            arguments[1],
+            arguments[2],
+            arguments[3],
+            arguments[4],
+            arguments[5],
+            arguments[6]
+          );
+      }
+    }
+
+    function createDateProperties() {
+      FakeDate.prototype = GlobalDate.prototype;
+
+      FakeDate.now = function() {
+        if (GlobalDate.now) {
+          return currentTime;
+        } else {
+          throw new Error('Browser does not support Date.now()');
+        }
+      };
+
+      FakeDate.toSource = GlobalDate.toSource;
+      FakeDate.toString = GlobalDate.toString;
+      FakeDate.parse = GlobalDate.parse;
+      FakeDate.UTC = GlobalDate.UTC;
+    }
+  }
+
+  return MockDate;
+};
+
+getJasmineRequireObj().makePrettyPrinter = function(j$) {
+  function SinglePrettyPrintRun(customObjectFormatters, pp) {
+    this.customObjectFormatters_ = customObjectFormatters;
+    this.ppNestLevel_ = 0;
+    this.seen = [];
+    this.length = 0;
+    this.stringParts = [];
+    this.pp_ = pp;
+  }
+
+  function hasCustomToString(value) {
+    // value.toString !== Object.prototype.toString if value has no custom toString but is from another context (e.g.
+    // iframe, web worker)
+    try {
+      return (
+        j$.isFunction_(value.toString) &&
+        value.toString !== Object.prototype.toString &&
+        value.toString() !== Object.prototype.toString.call(value)
+      );
+    } catch (e) {
+      // The custom toString() threw.
+      return true;
+    }
+  }
+
+  SinglePrettyPrintRun.prototype.format = function(value) {
+    this.ppNestLevel_++;
+    try {
+      var customFormatResult = this.applyCustomFormatters_(value);
+
+      if (customFormatResult) {
+        this.emitScalar(customFormatResult);
+      } else if (j$.util.isUndefined(value)) {
+        this.emitScalar('undefined');
+      } else if (value === null) {
+        this.emitScalar('null');
+      } else if (value === 0 && 1 / value === -Infinity) {
+        this.emitScalar('-0');
+      } else if (value === j$.getGlobal()) {
+        this.emitScalar('<global>');
+      } else if (value.jasmineToString) {
+        this.emitScalar(value.jasmineToString(this.pp_));
+      } else if (typeof value === 'string') {
+        this.emitString(value);
+      } else if (j$.isSpy(value)) {
+        this.emitScalar('spy on ' + value.and.identity);
+      } else if (j$.isSpy(value.toString)) {
+        this.emitScalar('spy on ' + value.toString.and.identity);
+      } else if (value instanceof RegExp) {
+        this.emitScalar(value.toString());
+      } else if (typeof value === 'function') {
+        this.emitScalar('Function');
+      } else if (j$.isDomNode(value)) {
+        if (value.tagName) {
+          this.emitDomElement(value);
+        } else {
+          this.emitScalar('HTMLNode');
+        }
+      } else if (value instanceof Date) {
+        this.emitScalar('Date(' + value + ')');
+      } else if (j$.isSet(value)) {
+        this.emitSet(value);
+      } else if (j$.isMap(value)) {
+        this.emitMap(value);
+      } else if (j$.isTypedArray_(value)) {
+        this.emitTypedArray(value);
+      } else if (
+        value.toString &&
+        typeof value === 'object' &&
+        !j$.isArray_(value) &&
+        hasCustomToString(value)
+      ) {
+        try {
+          this.emitScalar(value.toString());
+        } catch (e) {
+          this.emitScalar('has-invalid-toString-method');
+        }
+      } else if (j$.util.arrayContains(this.seen, value)) {
+        this.emitScalar(
+          '<circular reference: ' +
+            (j$.isArray_(value) ? 'Array' : 'Object') +
+            '>'
+        );
+      } else if (j$.isArray_(value) || j$.isA_('Object', value)) {
+        this.seen.push(value);
+        if (j$.isArray_(value)) {
+          this.emitArray(value);
+        } else {
+          this.emitObject(value);
+        }
+        this.seen.pop();
+      } else {
+        this.emitScalar(value.toString());
+      }
+    } catch (e) {
+      if (this.ppNestLevel_ > 1 || !(e instanceof MaxCharsReachedError)) {
+        throw e;
+      }
+    } finally {
+      this.ppNestLevel_--;
+    }
+  };
+
+  SinglePrettyPrintRun.prototype.applyCustomFormatters_ = function(value) {
+    return customFormat(value, this.customObjectFormatters_);
+  };
+
+  SinglePrettyPrintRun.prototype.iterateObject = function(obj, fn) {
+    var objKeys = keys(obj, j$.isArray_(obj));
+    var isGetter = function isGetter(prop) {};
+
+    if (obj.__lookupGetter__) {
+      isGetter = function isGetter(prop) {
+        var getter = obj.__lookupGetter__(prop);
+        return !j$.util.isUndefined(getter) && getter !== null;
+      };
+    }
+    var length = Math.min(objKeys.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH);
+    for (var i = 0; i < length; i++) {
+      var property = objKeys[i];
+      fn(property, isGetter(property));
+    }
+
+    return objKeys.length > length;
+  };
+
+  SinglePrettyPrintRun.prototype.emitScalar = function(value) {
+    this.append(value);
+  };
+
+  SinglePrettyPrintRun.prototype.emitString = function(value) {
+    this.append("'" + value + "'");
+  };
+
+  SinglePrettyPrintRun.prototype.emitArray = function(array) {
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      this.append('Array');
+      return;
+    }
+    var length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH);
+    this.append('[ ');
+    for (var i = 0; i < length; i++) {
+      if (i > 0) {
+        this.append(', ');
+      }
+      this.format(array[i]);
+    }
+    if (array.length > length) {
+      this.append(', ...');
+    }
+
+    var self = this;
+    var first = array.length === 0;
+    var truncated = this.iterateObject(array, function(property, isGetter) {
+      if (first) {
+        first = false;
+      } else {
+        self.append(', ');
+      }
+
+      self.formatProperty(array, property, isGetter);
+    });
+
+    if (truncated) {
+      this.append(', ...');
+    }
+
+    this.append(' ]');
+  };
+
+  SinglePrettyPrintRun.prototype.emitSet = function(set) {
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      this.append('Set');
+      return;
+    }
+    this.append('Set( ');
+    var size = Math.min(set.size, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH);
+    var i = 0;
+    set.forEach(function(value, key) {
+      if (i >= size) {
+        return;
+      }
+      if (i > 0) {
+        this.append(', ');
+      }
+      this.format(value);
+
+      i++;
+    }, this);
+    if (set.size > size) {
+      this.append(', ...');
+    }
+    this.append(' )');
+  };
+
+  SinglePrettyPrintRun.prototype.emitMap = function(map) {
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      this.append('Map');
+      return;
+    }
+    this.append('Map( ');
+    var size = Math.min(map.size, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH);
+    var i = 0;
+    map.forEach(function(value, key) {
+      if (i >= size) {
+        return;
+      }
+      if (i > 0) {
+        this.append(', ');
+      }
+      this.format([key, value]);
+
+      i++;
+    }, this);
+    if (map.size > size) {
+      this.append(', ...');
+    }
+    this.append(' )');
+  };
+
+  SinglePrettyPrintRun.prototype.emitObject = function(obj) {
+    var ctor = obj.constructor,
+      constructorName;
+
+    constructorName =
+      typeof ctor === 'function' && obj instanceof ctor
+        ? j$.fnNameFor(obj.constructor)
+        : 'null';
+
+    this.append(constructorName);
+
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      return;
+    }
+
+    var self = this;
+    this.append('({ ');
+    var first = true;
+
+    var truncated = this.iterateObject(obj, function(property, isGetter) {
+      if (first) {
+        first = false;
+      } else {
+        self.append(', ');
+      }
+
+      self.formatProperty(obj, property, isGetter);
+    });
+
+    if (truncated) {
+      this.append(', ...');
+    }
+
+    this.append(' })');
+  };
+
+  SinglePrettyPrintRun.prototype.emitTypedArray = function(arr) {
+    var constructorName = j$.fnNameFor(arr.constructor),
+      limitedArray = Array.prototype.slice.call(
+        arr,
+        0,
+        j$.MAX_PRETTY_PRINT_ARRAY_LENGTH
+      ),
+      itemsString = Array.prototype.join.call(limitedArray, ', ');
+
+    if (limitedArray.length !== arr.length) {
+      itemsString += ', ...';
+    }
+
+    this.append(constructorName + ' [ ' + itemsString + ' ]');
+  };
+
+  SinglePrettyPrintRun.prototype.emitDomElement = function(el) {
+    var tagName = el.tagName.toLowerCase(),
+      attrs = el.attributes,
+      i,
+      len = attrs.length,
+      out = '<' + tagName,
+      attr;
+
+    for (i = 0; i < len; i++) {
+      attr = attrs[i];
+      out += ' ' + attr.name;
+
+      if (attr.value !== '') {
+        out += '="' + attr.value + '"';
+      }
+    }
+
+    out += '>';
+
+    if (el.childElementCount !== 0 || el.textContent !== '') {
+      out += '...</' + tagName + '>';
+    }
+
+    this.append(out);
+  };
+
+  SinglePrettyPrintRun.prototype.formatProperty = function(
+    obj,
+    property,
+    isGetter
+  ) {
+    this.append(property);
+    this.append(': ');
+    if (isGetter) {
+      this.append('<getter>');
+    } else {
+      this.format(obj[property]);
+    }
+  };
+
+  SinglePrettyPrintRun.prototype.append = function(value) {
+    // This check protects us from the rare case where an object has overriden
+    // `toString()` with an invalid implementation (returning a non-string).
+    if (typeof value !== 'string') {
+      value = Object.prototype.toString.call(value);
+    }
+
+    var result = truncate(value, j$.MAX_PRETTY_PRINT_CHARS - this.length);
+    this.length += result.value.length;
+    this.stringParts.push(result.value);
+
+    if (result.truncated) {
+      throw new MaxCharsReachedError();
+    }
+  };
+
+  function truncate(s, maxlen) {
+    if (s.length <= maxlen) {
+      return { value: s, truncated: false };
+    }
+
+    s = s.substring(0, maxlen - 4) + ' ...';
+    return { value: s, truncated: true };
+  }
+
+  function MaxCharsReachedError() {
+    this.message =
+      'Exceeded ' +
+      j$.MAX_PRETTY_PRINT_CHARS +
+      ' characters while pretty-printing a value';
+  }
+
+  MaxCharsReachedError.prototype = new Error();
+
+  function keys(obj, isArray) {
+    var allKeys = Object.keys
+      ? Object.keys(obj)
+      : (function(o) {
+          var keys = [];
+          for (var key in o) {
+            if (j$.util.has(o, key)) {
+              keys.push(key);
+            }
+          }
+          return keys;
+        })(obj);
+
+    if (!isArray) {
+      return allKeys;
+    }
+
+    if (allKeys.length === 0) {
+      return allKeys;
+    }
+
+    var extraKeys = [];
+    for (var i = 0; i < allKeys.length; i++) {
+      if (!/^[0-9]+$/.test(allKeys[i])) {
+        extraKeys.push(allKeys[i]);
+      }
+    }
+
+    return extraKeys;
+  }
+
+  function customFormat(value, customObjectFormatters) {
+    var i, result;
+
+    for (i = 0; i < customObjectFormatters.length; i++) {
+      result = customObjectFormatters[i](value);
+
+      if (result !== undefined) {
+        return result;
+      }
+    }
+  }
+
+  return function(customObjectFormatters) {
+    customObjectFormatters = customObjectFormatters || [];
+
+    var pp = function(value) {
+      var prettyPrinter = new SinglePrettyPrintRun(customObjectFormatters, pp);
+      prettyPrinter.format(value);
+      return prettyPrinter.stringParts.join('');
+    };
+
+    pp.customFormat_ = function(value) {
+      return customFormat(value, customObjectFormatters);
+    };
+
+    return pp;
+  };
+};
+
+getJasmineRequireObj().QueueRunner = function(j$) {
+  var nextid = 1;
+
+  function StopExecutionError() {}
+  StopExecutionError.prototype = new Error();
+  j$.StopExecutionError = StopExecutionError;
+
+  function once(fn, onTwice) {
+    var called = false;
+    return function(arg) {
+      if (called) {
+        if (onTwice) {
+          onTwice();
+        }
+      } else {
+        called = true;
+        // Direct call using single parameter, because cleanup/next does not need more
+        fn(arg);
+      }
+      return null;
+    };
+  }
+
+  function fallbackOnMultipleDone() {
+    console.error(
+      new Error(
+        "An asynchronous function called its 'done' " +
+          'callback more than once, in a QueueRunner without a onMultipleDone ' +
+          'handler.'
+      )
+    );
+  }
+
+  function emptyFn() {}
+
+  function QueueRunner(attrs) {
+    this.id_ = nextid++;
+    var queueableFns = attrs.queueableFns || [];
+    this.queueableFns = queueableFns.concat(attrs.cleanupFns || []);
+    this.firstCleanupIx = queueableFns.length;
+    this.onComplete = attrs.onComplete || emptyFn;
+    this.clearStack =
+      attrs.clearStack ||
+      function(fn) {
+        fn();
+      };
+    this.onException = attrs.onException || emptyFn;
+    this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone;
+    this.userContext = attrs.userContext || new j$.UserContext();
+    this.timeout = attrs.timeout || {
+      setTimeout: setTimeout,
+      clearTimeout: clearTimeout
+    };
+    this.fail = attrs.fail || emptyFn;
+    this.globalErrors = attrs.globalErrors || {
+      pushListener: emptyFn,
+      popListener: emptyFn
+    };
+    this.completeOnFirstError = !!attrs.completeOnFirstError;
+    this.errored = false;
+
+    if (typeof this.onComplete !== 'function') {
+      throw new Error('invalid onComplete ' + JSON.stringify(this.onComplete));
+    }
+    this.deprecated = attrs.deprecated;
+  }
+
+  QueueRunner.prototype.execute = function() {
+    var self = this;
+    this.handleFinalError = function(message, source, lineno, colno, error) {
+      // Older browsers would send the error as the first parameter. HTML5
+      // specifies the the five parameters above. The error instance should
+      // be preffered, otherwise the call stack would get lost.
+      self.onException(error || message);
+    };
+    this.globalErrors.pushListener(this.handleFinalError);
+    this.run(0);
+  };
+
+  QueueRunner.prototype.skipToCleanup = function(lastRanIndex) {
+    if (lastRanIndex < this.firstCleanupIx) {
+      this.run(this.firstCleanupIx);
+    } else {
+      this.run(lastRanIndex + 1);
+    }
+  };
+
+  QueueRunner.prototype.clearTimeout = function(timeoutId) {
+    Function.prototype.apply.apply(this.timeout.clearTimeout, [
+      j$.getGlobal(),
+      [timeoutId]
+    ]);
+  };
+
+  QueueRunner.prototype.setTimeout = function(fn, timeout) {
+    return Function.prototype.apply.apply(this.timeout.setTimeout, [
+      j$.getGlobal(),
+      [fn, timeout]
+    ]);
+  };
+
+  QueueRunner.prototype.attempt = function attempt(iterativeIndex) {
+    var self = this,
+      completedSynchronously = true,
+      handleError = function handleError(error) {
+        // TODO probably shouldn't next() right away here.
+        // That makes debugging async failures much more confusing.
+        onException(error);
+      },
+      cleanup = once(function cleanup() {
+        if (timeoutId !== void 0) {
+          self.clearTimeout(timeoutId);
+        }
+        self.globalErrors.popListener(handleError);
+      }),
+      next = once(
+        function next(err) {
+          cleanup();
+
+          if (j$.isError_(err)) {
+            if (!(err instanceof StopExecutionError) && !err.jasmineMessage) {
+              self.fail(err);
+            }
+            self.errored = errored = true;
+          } else if (typeof err !== 'undefined' && !self.errored) {
+            self.deprecated(
+              'Any argument passed to a done callback will be treated as an ' +
+                'error in a future release. Call the done callback without ' +
+                "arguments if you don't want to trigger a spec failure."
+            );
+          }
+
+          function runNext() {
+            if (self.completeOnFirstError && errored) {
+              self.skipToCleanup(iterativeIndex);
+            } else {
+              self.run(iterativeIndex + 1);
+            }
+          }
+
+          if (completedSynchronously) {
+            self.setTimeout(runNext);
+          } else {
+            runNext();
+          }
+        },
+        function() {
+          try {
+            if (!timedOut) {
+              self.onMultipleDone();
+            }
+          } catch (error) {
+            // Any error we catch here is probably due to a bug in Jasmine,
+            // and it's not likely to end up anywhere useful if we let it
+            // propagate. Log it so it can at least show up when debugging.
+            console.error(error);
+          }
+        }
+      ),
+      errored = false,
+      timedOut = false,
+      queueableFn = self.queueableFns[iterativeIndex],
+      timeoutId,
+      maybeThenable;
+
+    next.fail = function nextFail() {
+      self.fail.apply(null, arguments);
+      self.errored = errored = true;
+      next();
+    };
+
+    self.globalErrors.pushListener(handleError);
+
+    if (queueableFn.timeout !== undefined) {
+      var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL;
+      timeoutId = self.setTimeout(function() {
+        timedOut = true;
+        var error = new Error(
+          'Timeout - Async function did not complete within ' +
+            timeoutInterval +
+            'ms ' +
+            (queueableFn.timeout
+              ? '(custom timeout)'
+              : '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)')
+        );
+        // TODO Need to decide what to do about a successful completion after a
+        //   timeout. That should probably not be a deprecation, and maybe not
+        //   an error in 4.0. (But a diagnostic of some sort might be helpful.)
+        onException(error);
+        next();
+      }, timeoutInterval);
+    }
+
+    try {
+      if (queueableFn.fn.length === 0) {
+        maybeThenable = queueableFn.fn.call(self.userContext);
+
+        if (maybeThenable && j$.isFunction_(maybeThenable.then)) {
+          maybeThenable.then(
+            wrapInPromiseResolutionHandler(next),
+            onPromiseRejection
+          );
+          completedSynchronously = false;
+          return { completedSynchronously: false };
+        }
+      } else {
+        maybeThenable = queueableFn.fn.call(self.userContext, next);
+        this.diagnoseConflictingAsync_(queueableFn.fn, maybeThenable);
+        completedSynchronously = false;
+        return { completedSynchronously: false };
+      }
+    } catch (e) {
+      onException(e);
+      self.errored = errored = true;
+    }
+
+    cleanup();
+    return { completedSynchronously: true, errored: errored };
+
+    function onException(e) {
+      self.onException(e);
+      self.errored = errored = true;
+    }
+
+    function onPromiseRejection(e) {
+      onException(e);
+      next();
+    }
+  };
+
+  QueueRunner.prototype.run = function(recursiveIndex) {
+    var length = this.queueableFns.length,
+      self = this,
+      iterativeIndex;
+
+    for (
+      iterativeIndex = recursiveIndex;
+      iterativeIndex < length;
+      iterativeIndex++
+    ) {
+      var result = this.attempt(iterativeIndex);
+
+      if (!result.completedSynchronously) {
+        return;
+      }
+
+      self.errored = self.errored || result.errored;
+
+      if (this.completeOnFirstError && result.errored) {
+        this.skipToCleanup(iterativeIndex);
+        return;
+      }
+    }
+
+    this.clearStack(function() {
+      self.globalErrors.popListener(self.handleFinalError);
+
+      if (self.errored) {
+        self.onComplete(new StopExecutionError());
+      } else {
+        self.onComplete();
+      }
+    });
+  };
+
+  QueueRunner.prototype.diagnoseConflictingAsync_ = function(fn, retval) {
+    var msg;
+
+    if (retval && j$.isFunction_(retval.then)) {
+      // Issue a warning that matches the user's code.
+      // Omit the stack trace because there's almost certainly no user code
+      // on the stack at this point.
+      if (j$.isAsyncFunction_(fn)) {
+        msg =
+          'An asynchronous before/it/after ' +
+          'function was defined with the async keyword but also took a ' +
+          'done callback. This is not supported and will stop working in' +
+          ' the future. Either remove the done callback (recommended) or ' +
+          'remove the async keyword.';
+      } else {
+        msg =
+          'An asynchronous before/it/after ' +
+          'function took a done callback but also returned a promise. ' +
+          'This is not supported and will stop working in the future. ' +
+          'Either remove the done callback (recommended) or change the ' +
+          'function to not return a promise.';
+      }
+
+      this.deprecated(msg, { omitStackTrace: true });
+    }
+  };
+
+  function wrapInPromiseResolutionHandler(fn) {
+    return function(maybeArg) {
+      if (j$.isError_(maybeArg)) {
+        fn(maybeArg);
+      } else {
+        fn();
+      }
+    };
+  }
+
+  return QueueRunner;
+};
+
+getJasmineRequireObj().ReportDispatcher = function(j$) {
+  function ReportDispatcher(methods, queueRunnerFactory, deprecated) {
+    var dispatchedMethods = methods || [];
+
+    for (var i = 0; i < dispatchedMethods.length; i++) {
+      var method = dispatchedMethods[i];
+      this[method] = (function(m) {
+        return function() {
+          dispatch(m, arguments);
+        };
+      })(method);
+    }
+
+    var reporters = [];
+    var fallbackReporter = null;
+
+    this.addReporter = function(reporter) {
+      reporters.push(reporter);
+    };
+
+    this.provideFallbackReporter = function(reporter) {
+      fallbackReporter = reporter;
+    };
+
+    this.clearReporters = function() {
+      reporters = [];
+    };
+
+    return this;
+
+    function dispatch(method, args) {
+      if (reporters.length === 0 && fallbackReporter !== null) {
+        reporters.push(fallbackReporter);
+      }
+      var onComplete = args[args.length - 1];
+      args = j$.util.argsToArray(args).splice(0, args.length - 1);
+      var fns = [];
+      for (var i = 0; i < reporters.length; i++) {
+        var reporter = reporters[i];
+        addFn(fns, reporter, method, args);
+      }
+
+      queueRunnerFactory({
+        queueableFns: fns,
+        onComplete: onComplete,
+        isReporter: true,
+        onMultipleDone: function() {
+          deprecated(
+            "An asynchronous reporter callback called its 'done' callback " +
+              'more than once. This is a bug in the reporter callback in ' +
+              'question. This will be treated as an error in a future ' +
+              'version. See' +
+              '<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> ' +
+              'for more information.',
+            { ignoreRunnable: true }
+          );
+        }
+      });
+    }
+
+    function addFn(fns, reporter, method, args) {
+      var fn = reporter[method];
+      if (!fn) {
+        return;
+      }
+
+      var thisArgs = j$.util.cloneArgs(args);
+      if (fn.length <= 1) {
+        fns.push({
+          fn: function() {
+            return fn.apply(reporter, thisArgs);
+          }
+        });
+      } else {
+        fns.push({
+          fn: function(done) {
+            return fn.apply(reporter, thisArgs.concat([done]));
+          }
+        });
+      }
+    }
+  }
+
+  return ReportDispatcher;
+};
+
+getJasmineRequireObj().interface = function(jasmine, env) {
+  var jasmineInterface = {
+    /**
+     * Callback passed to parts of the Jasmine base interface.
+     *
+     * By default Jasmine assumes this function completes synchronously.
+     * If you have code that you need to test asynchronously, you can declare that you receive a `done` callback, return a Promise, or use the `async` keyword if it is supported in your environment.
+     * @callback implementationCallback
+     * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
+     * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
+     */
+
+    /**
+     * Create a group of specs (often called a suite).
+     *
+     * Calls to `describe` can be nested within other calls to compose your suite as a tree.
+     * @name describe
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {String} description Textual description of the group
+     * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites and specs
+     */
+    describe: function(description, specDefinitions) {
+      return env.describe(description, specDefinitions);
+    },
+
+    /**
+     * A temporarily disabled [`describe`]{@link describe}
+     *
+     * Specs within an `xdescribe` will be marked pending and not executed
+     * @name xdescribe
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {String} description Textual description of the group
+     * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites and specs
+     */
+    xdescribe: function(description, specDefinitions) {
+      return env.xdescribe(description, specDefinitions);
+    },
+
+    /**
+     * A focused [`describe`]{@link describe}
+     *
+     * If suites or specs are focused, only those that are focused will be executed
+     * @see fit
+     * @name fdescribe
+     * @since 2.1.0
+     * @function
+     * @global
+     * @param {String} description Textual description of the group
+     * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites and specs
+     */
+    fdescribe: function(description, specDefinitions) {
+      return env.fdescribe(description, specDefinitions);
+    },
+
+    /**
+     * Define a single spec. A spec should contain one or more {@link expect|expectations} that test the state of the code.
+     *
+     * A spec whose expectations all succeed will be passing and a spec with any failures will fail.
+     * The name `it` is a pronoun for the test target, not an abbreviation of anything. It makes the
+     * spec more readable by connecting the function name `it` and the argument `description` as a
+     * complete sentence.
+     * @name it
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {String} description Textual description of what this spec is checking
+     * @param {implementationCallback} [testFunction] Function that contains the code of your test. If not provided the test will be `pending`.
+     * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async spec.
+     * @see async
+     */
+    it: function() {
+      return env.it.apply(env, arguments);
+    },
+
+    /**
+     * A temporarily disabled [`it`]{@link it}
+     *
+     * The spec will report as `pending` and will not be executed.
+     * @name xit
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {String} description Textual description of what this spec is checking.
+     * @param {implementationCallback} [testFunction] Function that contains the code of your test. Will not be executed.
+     */
+    xit: function() {
+      return env.xit.apply(env, arguments);
+    },
+
+    /**
+     * A focused [`it`]{@link it}
+     *
+     * If suites or specs are focused, only those that are focused will be executed.
+     * @name fit
+     * @since 2.1.0
+     * @function
+     * @global
+     * @param {String} description Textual description of what this spec is checking.
+     * @param {implementationCallback} testFunction Function that contains the code of your test.
+     * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async spec.
+     * @see async
+     */
+    fit: function() {
+      return env.fit.apply(env, arguments);
+    },
+
+    /**
+     * Run some shared setup before each of the specs in the {@link describe} in which it is called.
+     * @name beforeEach
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {implementationCallback} [function] Function that contains the code to setup your specs.
+     * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async beforeEach.
+     * @see async
+     */
+    beforeEach: function() {
+      return env.beforeEach.apply(env, arguments);
+    },
+
+    /**
+     * Run some shared teardown after each of the specs in the {@link describe} in which it is called.
+     * @name afterEach
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {implementationCallback} [function] Function that contains the code to teardown your specs.
+     * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async afterEach.
+     * @see async
+     */
+    afterEach: function() {
+      return env.afterEach.apply(env, arguments);
+    },
+
+    /**
+     * Run some shared setup once before all of the specs in the {@link describe} are run.
+     *
+     * _Note:_ Be careful, sharing the setup from a beforeAll makes it easy to accidentally leak state between your specs so that they erroneously pass or fail.
+     * @name beforeAll
+     * @since 2.1.0
+     * @function
+     * @global
+     * @param {implementationCallback} [function] Function that contains the code to setup your specs.
+     * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async beforeAll.
+     * @see async
+     */
+    beforeAll: function() {
+      return env.beforeAll.apply(env, arguments);
+    },
+
+    /**
+     * Run some shared teardown once after all of the specs in the {@link describe} are run.
+     *
+     * _Note:_ Be careful, sharing the teardown from a afterAll makes it easy to accidentally leak state between your specs so that they erroneously pass or fail.
+     * @name afterAll
+     * @since 2.1.0
+     * @function
+     * @global
+     * @param {implementationCallback} [function] Function that contains the code to teardown your specs.
+     * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async afterAll.
+     * @see async
+     */
+    afterAll: function() {
+      return env.afterAll.apply(env, arguments);
+    },
+
+    /**
+     * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult}
+     * @name setSpecProperty
+     * @since 3.6.0
+     * @function
+     * @param {String} key The name of the property
+     * @param {*} value The value of the property
+     */
+    setSpecProperty: function(key, value) {
+      return env.setSpecProperty(key, value);
+    },
+
+    /**
+     * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SuiteResult}
+     * @name setSuiteProperty
+     * @since 3.6.0
+     * @function
+     * @param {String} key The name of the property
+     * @param {*} value The value of the property
+     */
+    setSuiteProperty: function(key, value) {
+      return env.setSuiteProperty(key, value);
+    },
+
+    /**
+     * Create an expectation for a spec.
+     * @name expect
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {Object} actual - Actual computed value to test expectations against.
+     * @return {matchers}
+     */
+    expect: function(actual) {
+      return env.expect(actual);
+    },
+
+    /**
+     * Create an asynchronous expectation for a spec. Note that the matchers
+     * that are provided by an asynchronous expectation all return promises
+     * which must be either returned from the spec or waited for using `await`
+     * in order for Jasmine to associate them with the correct spec.
+     * @name expectAsync
+     * @since 3.3.0
+     * @function
+     * @global
+     * @param {Object} actual - Actual computed value to test expectations against.
+     * @return {async-matchers}
+     * @example
+     * await expectAsync(somePromise).toBeResolved();
+     * @example
+     * return expectAsync(somePromise).toBeResolved();
+     */
+    expectAsync: function(actual) {
+      return env.expectAsync(actual);
+    },
+
+    /**
+     * Mark a spec as pending, expectation results will be ignored.
+     * @name pending
+     * @since 2.0.0
+     * @function
+     * @global
+     * @param {String} [message] - Reason the spec is pending.
+     */
+    pending: function() {
+      return env.pending.apply(env, arguments);
+    },
+
+    /**
+     * Explicitly mark a spec as failed.
+     * @name fail
+     * @since 2.1.0
+     * @function
+     * @global
+     * @param {String|Error} [error] - Reason for the failure.
+     */
+    fail: function() {
+      return env.fail.apply(env, arguments);
+    },
+
+    /**
+     * Install a spy onto an existing object.
+     * @name spyOn
+     * @since 1.3.0
+     * @function
+     * @global
+     * @param {Object} obj - The object upon which to install the {@link Spy}.
+     * @param {String} methodName - The name of the method to replace with a {@link Spy}.
+     * @returns {Spy}
+     */
+    spyOn: function(obj, methodName) {
+      return env.spyOn(obj, methodName);
+    },
+
+    /**
+     * Install a spy on a property installed with `Object.defineProperty` onto an existing object.
+     * @name spyOnProperty
+     * @since 2.6.0
+     * @function
+     * @global
+     * @param {Object} obj - The object upon which to install the {@link Spy}
+     * @param {String} propertyName - The name of the property to replace with a {@link Spy}.
+     * @param {String} [accessType=get] - The access type (get|set) of the property to {@link Spy} on.
+     * @returns {Spy}
+     */
+    spyOnProperty: function(obj, methodName, accessType) {
+      return env.spyOnProperty(obj, methodName, accessType);
+    },
+
+    /**
+     * Installs spies on all writable and configurable properties of an object.
+     * @name spyOnAllFunctions
+     * @since 3.2.1
+     * @function
+     * @global
+     * @param {Object} obj - The object upon which to install the {@link Spy}s
+     * @param {boolean} includeNonEnumerable - Whether or not to add spies to non-enumerable properties
+     * @returns {Object} the spied object
+     */
+    spyOnAllFunctions: function(obj, includeNonEnumerable) {
+      return env.spyOnAllFunctions(obj, includeNonEnumerable);
+    },
+
+    jsApiReporter: new jasmine.JsApiReporter({
+      timer: new jasmine.Timer()
+    }),
+
+    /**
+     * @namespace jasmine
+     */
+    jasmine: jasmine
+  };
+
+  /**
+   * Add a custom equality tester for the current scope of specs.
+   *
+   * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
+   * @name jasmine.addCustomEqualityTester
+   * @since 2.0.0
+   * @function
+   * @param {Function} tester - A function which takes two arguments to compare and returns a `true` or `false` comparison result if it knows how to compare them, and `undefined` otherwise.
+   * @see custom_equality
+   */
+  jasmine.addCustomEqualityTester = function(tester) {
+    env.addCustomEqualityTester(tester);
+  };
+
+  /**
+   * Add custom matchers for the current scope of specs.
+   *
+   * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
+   * @name jasmine.addMatchers
+   * @since 2.0.0
+   * @function
+   * @param {Object} matchers - Keys from this object will be the new matcher names.
+   * @see custom_matcher
+   */
+  jasmine.addMatchers = function(matchers) {
+    return env.addMatchers(matchers);
+  };
+
+  /**
+   * Add custom async matchers for the current scope of specs.
+   *
+   * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
+   * @name jasmine.addAsyncMatchers
+   * @since 3.5.0
+   * @function
+   * @param {Object} matchers - Keys from this object will be the new async matcher names.
+   * @see custom_matcher
+   */
+  jasmine.addAsyncMatchers = function(matchers) {
+    return env.addAsyncMatchers(matchers);
+  };
+
+  /**
+   * Add a custom object formatter for the current scope of specs.
+   *
+   * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
+   * @name jasmine.addCustomObjectFormatter
+   * @since 3.6.0
+   * @function
+   * @param {Function} formatter - A function which takes a value to format and returns a string if it knows how to format it, and `undefined` otherwise.
+   * @see custom_object_formatters
+   */
+  jasmine.addCustomObjectFormatter = function(formatter) {
+    return env.addCustomObjectFormatter(formatter);
+  };
+
+  /**
+   * Get the currently booted mock {Clock} for this Jasmine environment.
+   * @name jasmine.clock
+   * @since 2.0.0
+   * @function
+   * @returns {Clock}
+   */
+  jasmine.clock = function() {
+    return env.clock;
+  };
+
+  /**
+   * Create a bare {@link Spy} object. This won't be installed anywhere and will not have any implementation behind it.
+   * @name jasmine.createSpy
+   * @since 1.3.0
+   * @function
+   * @param {String} [name] - Name to give the spy. This will be displayed in failure messages.
+   * @param {Function} [originalFn] - Function to act as the real implementation.
+   * @return {Spy}
+   */
+  jasmine.createSpy = function(name, originalFn) {
+    return env.createSpy(name, originalFn);
+  };
+
+  /**
+   * Create an object with multiple {@link Spy}s as its members.
+   * @name jasmine.createSpyObj
+   * @since 1.3.0
+   * @function
+   * @param {String} [baseName] - Base name for the spies in the object.
+   * @param {String[]|Object} methodNames - Array of method names to create spies for, or Object whose keys will be method names and values the {@link Spy#and#returnValue|returnValue}.
+   * @param {String[]|Object} [propertyNames] - Array of property names to create spies for, or Object whose keys will be propertynames and values the {@link Spy#and#returnValue|returnValue}.
+   * @return {Object}
+   */
+  jasmine.createSpyObj = function(baseName, methodNames, propertyNames) {
+    return env.createSpyObj(baseName, methodNames, propertyNames);
+  };
+
+  /**
+   * Add a custom spy strategy for the current scope of specs.
+   *
+   * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
+   * @name jasmine.addSpyStrategy
+   * @since 3.5.0
+   * @function
+   * @param {String} name - The name of the strategy (i.e. what you call from `and`)
+   * @param {Function} factory - Factory function that returns the plan to be executed.
+   */
+  jasmine.addSpyStrategy = function(name, factory) {
+    return env.addSpyStrategy(name, factory);
+  };
+
+  /**
+   * Set the default spy strategy for the current scope of specs.
+   *
+   * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
+   * @name jasmine.setDefaultSpyStrategy
+   * @function
+   * @param {Function} defaultStrategyFn - a function that assigns a strategy
+   * @example
+   * beforeEach(function() {
+   *   jasmine.setDefaultSpyStrategy(and => and.returnValue(true));
+   * });
+   */
+  jasmine.setDefaultSpyStrategy = function(defaultStrategyFn) {
+    return env.setDefaultSpyStrategy(defaultStrategyFn);
+  };
+
+  return jasmineInterface;
+};
+
+getJasmineRequireObj().Spy = function(j$) {
+  var nextOrder = (function() {
+    var order = 0;
+
+    return function() {
+      return order++;
+    };
+  })();
+
+  var matchersUtil = new j$.MatchersUtil({
+    customTesters: [],
+    pp: j$.makePrettyPrinter()
+  });
+
+  /**
+   * @classdesc _Note:_ Do not construct this directly. Use {@link spyOn},
+   * {@link spyOnProperty}, {@link jasmine.createSpy}, or
+   * {@link jasmine.createSpyObj} instead.
+   * @class Spy
+   * @hideconstructor
+   */
+  function Spy(
+    name,
+    originalFn,
+    customStrategies,
+    defaultStrategyFn,
+    getPromise
+  ) {
+    var numArgs = typeof originalFn === 'function' ? originalFn.length : 0,
+      wrapper = makeFunc(numArgs, function(context, args, invokeNew) {
+        return spy(context, args, invokeNew);
+      }),
+      strategyDispatcher = new SpyStrategyDispatcher({
+        name: name,
+        fn: originalFn,
+        getSpy: function() {
+          return wrapper;
+        },
+        customStrategies: customStrategies,
+        getPromise: getPromise
+      }),
+      callTracker = new j$.CallTracker(),
+      spy = function(context, args, invokeNew) {
+        /**
+         * @name Spy.callData
+         * @property {object} object - `this` context for the invocation.
+         * @property {number} invocationOrder - Order of the invocation.
+         * @property {Array} args - The arguments passed for this invocation.
+         * @property returnValue - The value that was returned from this invocation.
+         */
+        var callData = {
+          object: context,
+          invocationOrder: nextOrder(),
+          args: Array.prototype.slice.apply(args)
+        };
+
+        callTracker.track(callData);
+        var returnValue = strategyDispatcher.exec(context, args, invokeNew);
+        callData.returnValue = returnValue;
+
+        return returnValue;
+      };
+
+    function makeFunc(length, fn) {
+      switch (length) {
+        case 1:
+          return function wrap1(a) {
+            return fn(this, arguments, this instanceof wrap1);
+          };
+        case 2:
+          return function wrap2(a, b) {
+            return fn(this, arguments, this instanceof wrap2);
+          };
+        case 3:
+          return function wrap3(a, b, c) {
+            return fn(this, arguments, this instanceof wrap3);
+          };
+        case 4:
+          return function wrap4(a, b, c, d) {
+            return fn(this, arguments, this instanceof wrap4);
+          };
+        case 5:
+          return function wrap5(a, b, c, d, e) {
+            return fn(this, arguments, this instanceof wrap5);
+          };
+        case 6:
+          return function wrap6(a, b, c, d, e, f) {
+            return fn(this, arguments, this instanceof wrap6);
+          };
+        case 7:
+          return function wrap7(a, b, c, d, e, f, g) {
+            return fn(this, arguments, this instanceof wrap7);
+          };
+        case 8:
+          return function wrap8(a, b, c, d, e, f, g, h) {
+            return fn(this, arguments, this instanceof wrap8);
+          };
+        case 9:
+          return function wrap9(a, b, c, d, e, f, g, h, i) {
+            return fn(this, arguments, this instanceof wrap9);
+          };
+        default:
+          return function wrap() {
+            return fn(this, arguments, this instanceof wrap);
+          };
+      }
+    }
+
+    for (var prop in originalFn) {
+      if (prop === 'and' || prop === 'calls') {
+        throw new Error(
+          "Jasmine spies would overwrite the 'and' and 'calls' properties on the object being spied upon"
+        );
+      }
+
+      wrapper[prop] = originalFn[prop];
+    }
+
+    /**
+     * @member {SpyStrategy} - Accesses the default strategy for the spy. This strategy will be used
+     * whenever the spy is called with arguments that don't match any strategy
+     * created with {@link Spy#withArgs}.
+     * @name Spy#and
+     * @since 2.0.0
+     * @example
+     * spyOn(someObj, 'func').and.returnValue(42);
+     */
+    wrapper.and = strategyDispatcher.and;
+    /**
+     * Specifies a strategy to be used for calls to the spy that have the
+     * specified arguments.
+     * @name Spy#withArgs
+     * @since 3.0.0
+     * @function
+     * @param {...*} args - The arguments to match
+     * @type {SpyStrategy}
+     * @example
+     * spyOn(someObj, 'func').withArgs(1, 2, 3).and.returnValue(42);
+     * someObj.func(1, 2, 3); // returns 42
+     */
+    wrapper.withArgs = function() {
+      return strategyDispatcher.withArgs.apply(strategyDispatcher, arguments);
+    };
+    wrapper.calls = callTracker;
+
+    if (defaultStrategyFn) {
+      defaultStrategyFn(wrapper.and);
+    }
+
+    return wrapper;
+  }
+
+  function SpyStrategyDispatcher(strategyArgs) {
+    var baseStrategy = new j$.SpyStrategy(strategyArgs);
+    var argsStrategies = new StrategyDict(function() {
+      return new j$.SpyStrategy(strategyArgs);
+    });
+
+    this.and = baseStrategy;
+
+    this.exec = function(spy, args, invokeNew) {
+      var strategy = argsStrategies.get(args);
+
+      if (!strategy) {
+        if (argsStrategies.any() && !baseStrategy.isConfigured()) {
+          throw new Error(
+            "Spy '" +
+              strategyArgs.name +
+              "' received a call with arguments " +
+              j$.basicPrettyPrinter_(Array.prototype.slice.call(args)) +
+              ' but all configured strategies specify other arguments.'
+          );
+        } else {
+          strategy = baseStrategy;
+        }
+      }
+
+      return strategy.exec(spy, args, invokeNew);
+    };
+
+    this.withArgs = function() {
+      return { and: argsStrategies.getOrCreate(arguments) };
+    };
+  }
+
+  function StrategyDict(strategyFactory) {
+    this.strategies = [];
+    this.strategyFactory = strategyFactory;
+  }
+
+  StrategyDict.prototype.any = function() {
+    return this.strategies.length > 0;
+  };
+
+  StrategyDict.prototype.getOrCreate = function(args) {
+    var strategy = this.get(args);
+
+    if (!strategy) {
+      strategy = this.strategyFactory();
+      this.strategies.push({
+        args: args,
+        strategy: strategy
+      });
+    }
+
+    return strategy;
+  };
+
+  StrategyDict.prototype.get = function(args) {
+    var i;
+
+    for (i = 0; i < this.strategies.length; i++) {
+      if (matchersUtil.equals(args, this.strategies[i].args)) {
+        return this.strategies[i].strategy;
+      }
+    }
+  };
+
+  return Spy;
+};
+
+getJasmineRequireObj().SpyFactory = function(j$) {
+  function SpyFactory(getCustomStrategies, getDefaultStrategyFn, getPromise) {
+    var self = this;
+
+    this.createSpy = function(name, originalFn) {
+      return j$.Spy(
+        name,
+        originalFn,
+        getCustomStrategies(),
+        getDefaultStrategyFn(),
+        getPromise
+      );
+    };
+
+    this.createSpyObj = function(baseName, methodNames, propertyNames) {
+      var baseNameIsCollection =
+        j$.isObject_(baseName) || j$.isArray_(baseName);
+
+      if (baseNameIsCollection) {
+        propertyNames = methodNames;
+        methodNames = baseName;
+        baseName = 'unknown';
+      }
+
+      var obj = {};
+      var spy, descriptor;
+
+      var methods = normalizeKeyValues(methodNames);
+      for (var i = 0; i < methods.length; i++) {
+        spy = obj[methods[i][0]] = self.createSpy(
+          baseName + '.' + methods[i][0]
+        );
+        if (methods[i].length > 1) {
+          spy.and.returnValue(methods[i][1]);
+        }
+      }
+
+      var properties = normalizeKeyValues(propertyNames);
+      for (var i = 0; i < properties.length; i++) {
+        descriptor = {
+          enumerable: true,
+          get: self.createSpy(baseName + '.' + properties[i][0] + '.get'),
+          set: self.createSpy(baseName + '.' + properties[i][0] + '.set')
+        };
+        if (properties[i].length > 1) {
+          descriptor.get.and.returnValue(properties[i][1]);
+          descriptor.set.and.returnValue(properties[i][1]);
+        }
+        Object.defineProperty(obj, properties[i][0], descriptor);
+      }
+
+      if (methods.length === 0 && properties.length === 0) {
+        throw 'createSpyObj requires a non-empty array or object of method names to create spies for';
+      }
+
+      return obj;
+    };
+  }
+
+  function normalizeKeyValues(object) {
+    var result = [];
+    if (j$.isArray_(object)) {
+      for (var i = 0; i < object.length; i++) {
+        result.push([object[i]]);
+      }
+    } else if (j$.isObject_(object)) {
+      for (var key in object) {
+        if (object.hasOwnProperty(key)) {
+          result.push([key, object[key]]);
+        }
+      }
+    }
+    return result;
+  }
+
+  return SpyFactory;
+};
+
+getJasmineRequireObj().SpyRegistry = function(j$) {
+  var spyOnMsg = j$.formatErrorMsg('<spyOn>', 'spyOn(<object>, <methodName>)');
+  var spyOnPropertyMsg = j$.formatErrorMsg(
+    '<spyOnProperty>',
+    'spyOnProperty(<object>, <propName>, [accessType])'
+  );
+
+  function SpyRegistry(options) {
+    options = options || {};
+    var global = options.global || j$.getGlobal();
+    var createSpy = options.createSpy;
+    var currentSpies =
+      options.currentSpies ||
+      function() {
+        return [];
+      };
+
+    this.allowRespy = function(allow) {
+      this.respy = allow;
+    };
+
+    this.spyOn = function(obj, methodName) {
+      var getErrorMsg = spyOnMsg;
+
+      if (j$.util.isUndefined(obj) || obj === null) {
+        throw new Error(
+          getErrorMsg(
+            'could not find an object to spy upon for ' + methodName + '()'
+          )
+        );
+      }
+
+      if (j$.util.isUndefined(methodName) || methodName === null) {
+        throw new Error(getErrorMsg('No method name supplied'));
+      }
+
+      if (j$.util.isUndefined(obj[methodName])) {
+        throw new Error(getErrorMsg(methodName + '() method does not exist'));
+      }
+
+      if (obj[methodName] && j$.isSpy(obj[methodName])) {
+        if (this.respy) {
+          return obj[methodName];
+        } else {
+          throw new Error(
+            getErrorMsg(methodName + ' has already been spied upon')
+          );
+        }
+      }
+
+      var descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
+
+      if (descriptor && !(descriptor.writable || descriptor.set)) {
+        throw new Error(
+          getErrorMsg(methodName + ' is not declared writable or has no setter')
+        );
+      }
+
+      var originalMethod = obj[methodName],
+        spiedMethod = createSpy(methodName, originalMethod),
+        restoreStrategy;
+
+      if (
+        Object.prototype.hasOwnProperty.call(obj, methodName) ||
+        (obj === global && methodName === 'onerror')
+      ) {
+        restoreStrategy = function() {
+          obj[methodName] = originalMethod;
+        };
+      } else {
+        restoreStrategy = function() {
+          if (!delete obj[methodName]) {
+            obj[methodName] = originalMethod;
+          }
+        };
+      }
+
+      currentSpies().push({
+        restoreObjectToOriginalState: restoreStrategy
+      });
+
+      obj[methodName] = spiedMethod;
+
+      return spiedMethod;
+    };
+
+    this.spyOnProperty = function(obj, propertyName, accessType) {
+      var getErrorMsg = spyOnPropertyMsg;
+
+      accessType = accessType || 'get';
+
+      if (j$.util.isUndefined(obj)) {
+        throw new Error(
+          getErrorMsg(
+            'spyOn could not find an object to spy upon for ' +
+              propertyName +
+              ''
+          )
+        );
+      }
+
+      if (j$.util.isUndefined(propertyName)) {
+        throw new Error(getErrorMsg('No property name supplied'));
+      }
+
+      var descriptor = j$.util.getPropertyDescriptor(obj, propertyName);
+
+      if (!descriptor) {
+        throw new Error(getErrorMsg(propertyName + ' property does not exist'));
+      }
+
+      if (!descriptor.configurable) {
+        throw new Error(
+          getErrorMsg(propertyName + ' is not declared configurable')
+        );
+      }
+
+      if (!descriptor[accessType]) {
+        throw new Error(
+          getErrorMsg(
+            'Property ' +
+              propertyName +
+              ' does not have access type ' +
+              accessType
+          )
+        );
+      }
+
+      if (j$.isSpy(descriptor[accessType])) {
+        if (this.respy) {
+          return descriptor[accessType];
+        } else {
+          throw new Error(
+            getErrorMsg(
+              propertyName + '#' + accessType + ' has already been spied upon'
+            )
+          );
+        }
+      }
+
+      var originalDescriptor = j$.util.clone(descriptor),
+        spy = createSpy(propertyName, descriptor[accessType]),
+        restoreStrategy;
+
+      if (Object.prototype.hasOwnProperty.call(obj, propertyName)) {
+        restoreStrategy = function() {
+          Object.defineProperty(obj, propertyName, originalDescriptor);
+        };
+      } else {
+        restoreStrategy = function() {
+          delete obj[propertyName];
+        };
+      }
+
+      currentSpies().push({
+        restoreObjectToOriginalState: restoreStrategy
+      });
+
+      descriptor[accessType] = spy;
+
+      Object.defineProperty(obj, propertyName, descriptor);
+
+      return spy;
+    };
+
+    this.spyOnAllFunctions = function(obj, includeNonEnumerable) {
+      if (j$.util.isUndefined(obj)) {
+        throw new Error(
+          'spyOnAllFunctions could not find an object to spy upon'
+        );
+      }
+
+      var pointer = obj,
+        propsToSpyOn = [],
+        properties,
+        propertiesToSkip = [];
+
+      while (
+        pointer &&
+        (!includeNonEnumerable || pointer !== Object.prototype)
+      ) {
+        properties = getProps(pointer, includeNonEnumerable);
+        properties = properties.filter(function(prop) {
+          return propertiesToSkip.indexOf(prop) === -1;
+        });
+        propertiesToSkip = propertiesToSkip.concat(properties);
+        propsToSpyOn = propsToSpyOn.concat(
+          getSpyableFunctionProps(pointer, properties)
+        );
+        pointer = Object.getPrototypeOf(pointer);
+      }
+
+      for (var i = 0; i < propsToSpyOn.length; i++) {
+        this.spyOn(obj, propsToSpyOn[i]);
+      }
+
+      return obj;
+    };
+
+    this.clearSpies = function() {
+      var spies = currentSpies();
+      for (var i = spies.length - 1; i >= 0; i--) {
+        var spyEntry = spies[i];
+        spyEntry.restoreObjectToOriginalState();
+      }
+    };
+  }
+
+  function getProps(obj, includeNonEnumerable) {
+    var enumerableProperties = Object.keys(obj);
+
+    if (!includeNonEnumerable) {
+      return enumerableProperties;
+    }
+
+    return Object.getOwnPropertyNames(obj).filter(function(prop) {
+      return (
+        prop !== 'constructor' ||
+        enumerableProperties.indexOf('constructor') > -1
+      );
+    });
+  }
+
+  function getSpyableFunctionProps(obj, propertiesToCheck) {
+    var props = [],
+      prop;
+    for (var i = 0; i < propertiesToCheck.length; i++) {
+      prop = propertiesToCheck[i];
+      if (
+        Object.prototype.hasOwnProperty.call(obj, prop) &&
+        isSpyableProp(obj, prop)
+      ) {
+        props.push(prop);
+      }
+    }
+    return props;
+  }
+
+  function isSpyableProp(obj, prop) {
+    var value, descriptor;
+    try {
+      value = obj[prop];
+    } catch (e) {
+      return false;
+    }
+    if (value instanceof Function) {
+      descriptor = Object.getOwnPropertyDescriptor(obj, prop);
+      return (descriptor.writable || descriptor.set) && descriptor.configurable;
+    }
+    return false;
+  }
+
+  return SpyRegistry;
+};
+
+getJasmineRequireObj().SpyStrategy = function(j$) {
+  /**
+   * @interface SpyStrategy
+   */
+  function SpyStrategy(options) {
+    options = options || {};
+
+    var self = this;
+
+    /**
+     * Get the identifying information for the spy.
+     * @name SpyStrategy#identity
+     * @since 3.0.0
+     * @member
+     * @type {String}
+     */
+    this.identity = options.name || 'unknown';
+    this.originalFn = options.fn || function() {};
+    this.getSpy = options.getSpy || function() {};
+    this.plan = this._defaultPlan = function() {};
+
+    var k,
+      cs = options.customStrategies || {};
+    for (k in cs) {
+      if (j$.util.has(cs, k) && !this[k]) {
+        this[k] = createCustomPlan(cs[k]);
+      }
+    }
+
+    var getPromise =
+      typeof options.getPromise === 'function'
+        ? options.getPromise
+        : function() {};
+
+    var requirePromise = function(name) {
+      var Promise = getPromise();
+
+      if (!Promise) {
+        throw new Error(
+          name +
+            ' requires global Promise, or `Promise` configured with `jasmine.getEnv().configure()`'
+        );
+      }
+
+      return Promise;
+    };
+
+    /**
+     * Tell the spy to return a promise resolving to the specified value when invoked.
+     * @name SpyStrategy#resolveTo
+     * @since 3.5.0
+     * @function
+     * @param {*} value The value to return.
+     */
+    this.resolveTo = function(value) {
+      var Promise = requirePromise('resolveTo');
+      self.plan = function() {
+        return Promise.resolve(value);
+      };
+      return self.getSpy();
+    };
+
+    /**
+     * Tell the spy to return a promise rejecting with the specified value when invoked.
+     * @name SpyStrategy#rejectWith
+     * @since 3.5.0
+     * @function
+     * @param {*} value The value to return.
+     */
+    this.rejectWith = function(value) {
+      var Promise = requirePromise('rejectWith');
+
+      self.plan = function() {
+        return Promise.reject(value);
+      };
+      return self.getSpy();
+    };
+  }
+
+  function createCustomPlan(factory) {
+    return function() {
+      var plan = factory.apply(null, arguments);
+
+      if (!j$.isFunction_(plan)) {
+        throw new Error('Spy strategy must return a function');
+      }
+
+      this.plan = plan;
+      return this.getSpy();
+    };
+  }
+
+  /**
+   * Execute the current spy strategy.
+   * @name SpyStrategy#exec
+   * @since 2.0.0
+   * @function
+   */
+  SpyStrategy.prototype.exec = function(context, args, invokeNew) {
+    var contextArgs = [context].concat(
+      args ? Array.prototype.slice.call(args) : []
+    );
+    var target = this.plan.bind.apply(this.plan, contextArgs);
+
+    return invokeNew ? new target() : target();
+  };
+
+  /**
+   * Tell the spy to call through to the real implementation when invoked.
+   * @name SpyStrategy#callThrough
+   * @since 2.0.0
+   * @function
+   */
+  SpyStrategy.prototype.callThrough = function() {
+    this.plan = this.originalFn;
+    return this.getSpy();
+  };
+
+  /**
+   * Tell the spy to return the value when invoked.
+   * @name SpyStrategy#returnValue
+   * @since 2.0.0
+   * @function
+   * @param {*} value The value to return.
+   */
+  SpyStrategy.prototype.returnValue = function(value) {
+    this.plan = function() {
+      return value;
+    };
+    return this.getSpy();
+  };
+
+  /**
+   * Tell the spy to return one of the specified values (sequentially) each time the spy is invoked.
+   * @name SpyStrategy#returnValues
+   * @since 2.1.0
+   * @function
+   * @param {...*} values - Values to be returned on subsequent calls to the spy.
+   */
+  SpyStrategy.prototype.returnValues = function() {
+    var values = Array.prototype.slice.call(arguments);
+    this.plan = function() {
+      return values.shift();
+    };
+    return this.getSpy();
+  };
+
+  /**
+   * Tell the spy to throw an error when invoked.
+   * @name SpyStrategy#throwError
+   * @since 2.0.0
+   * @function
+   * @param {Error|Object|String} something Thing to throw
+   */
+  SpyStrategy.prototype.throwError = function(something) {
+    var error = j$.isString_(something) ? new Error(something) : something;
+    this.plan = function() {
+      throw error;
+    };
+    return this.getSpy();
+  };
+
+  /**
+   * Tell the spy to call a fake implementation when invoked.
+   * @name SpyStrategy#callFake
+   * @since 2.0.0
+   * @function
+   * @param {Function} fn The function to invoke with the passed parameters.
+   */
+  SpyStrategy.prototype.callFake = function(fn) {
+    if (
+      !(
+        j$.isFunction_(fn) ||
+        j$.isAsyncFunction_(fn) ||
+        j$.isGeneratorFunction_(fn)
+      )
+    ) {
+      throw new Error(
+        'Argument passed to callFake should be a function, got ' + fn
+      );
+    }
+    this.plan = fn;
+    return this.getSpy();
+  };
+
+  /**
+   * Tell the spy to do nothing when invoked. This is the default.
+   * @name SpyStrategy#stub
+   * @since 2.0.0
+   * @function
+   */
+  SpyStrategy.prototype.stub = function(fn) {
+    this.plan = function() {};
+    return this.getSpy();
+  };
+
+  SpyStrategy.prototype.isConfigured = function() {
+    return this.plan !== this._defaultPlan;
+  };
+
+  return SpyStrategy;
+};
+
+getJasmineRequireObj().StackTrace = function(j$) {
+  function StackTrace(error) {
+    var lines = error.stack.split('\n').filter(function(line) {
+      return line !== '';
+    });
+
+    var extractResult = extractMessage(error.message, lines);
+
+    if (extractResult) {
+      this.message = extractResult.message;
+      lines = extractResult.remainder;
+    }
+
+    var parseResult = tryParseFrames(lines);
+    this.frames = parseResult.frames;
+    this.style = parseResult.style;
+  }
+
+  var framePatterns = [
+    // PhantomJS on Linux, Node, Chrome, IE, Edge
+    // e.g. "   at QueueRunner.run (http://localhost:8888/__jasmine__/jasmine.js:4320:20)"
+    // Note that the "function name" can include a surprisingly large set of
+    // characters, including angle brackets and square brackets.
+    {
+      re: /^\s*at ([^\)]+) \(([^\)]+)\)$/,
+      fnIx: 1,
+      fileLineColIx: 2,
+      style: 'v8'
+    },
+
+    // NodeJS alternate form, often mixed in with the Chrome style
+    // e.g. "  at /some/path:4320:20
+    { re: /\s*at (.+)$/, fileLineColIx: 1, style: 'v8' },
+
+    // PhantomJS on OS X, Safari, Firefox
+    // e.g. "run@http://localhost:8888/__jasmine__/jasmine.js:4320:27"
+    // or "http://localhost:8888/__jasmine__/jasmine.js:4320:27"
+    {
+      re: /^(([^@\s]+)@)?([^\s]+)$/,
+      fnIx: 2,
+      fileLineColIx: 3,
+      style: 'webkit'
+    }
+  ];
+
+  // regexes should capture the function name (if any) as group 1
+  // and the file, line, and column as group 2.
+  function tryParseFrames(lines) {
+    var style = null;
+    var frames = lines.map(function(line) {
+      var convertedLine = first(framePatterns, function(pattern) {
+        var overallMatch = line.match(pattern.re),
+          fileLineColMatch;
+        if (!overallMatch) {
+          return null;
+        }
+
+        fileLineColMatch = overallMatch[pattern.fileLineColIx].match(
+          /^(.*):(\d+):\d+$/
+        );
+        if (!fileLineColMatch) {
+          return null;
+        }
+
+        style = style || pattern.style;
+        return {
+          raw: line,
+          file: fileLineColMatch[1],
+          line: parseInt(fileLineColMatch[2], 10),
+          func: overallMatch[pattern.fnIx]
+        };
+      });
+
+      return convertedLine || { raw: line };
+    });
+
+    return {
+      style: style,
+      frames: frames
+    };
+  }
+
+  function first(items, fn) {
+    var i, result;
+
+    for (i = 0; i < items.length; i++) {
+      result = fn(items[i]);
+
+      if (result) {
+        return result;
+      }
+    }
+  }
+
+  function extractMessage(message, stackLines) {
+    var len = messagePrefixLength(message, stackLines);
+
+    if (len > 0) {
+      return {
+        message: stackLines.slice(0, len).join('\n'),
+        remainder: stackLines.slice(len)
+      };
+    }
+  }
+
+  function messagePrefixLength(message, stackLines) {
+    if (!stackLines[0].match(/^\w*Error/)) {
+      return 0;
+    }
+
+    var messageLines = message.split('\n');
+    var i;
+
+    for (i = 1; i < messageLines.length; i++) {
+      if (messageLines[i] !== stackLines[i]) {
+        return 0;
+      }
+    }
+
+    return messageLines.length;
+  }
+
+  return StackTrace;
+};
+
+getJasmineRequireObj().Suite = function(j$) {
+  /**
+   * @interface Suite
+   * @see Env#topSuite
+   * @since 2.0.0
+   */
+  function Suite(attrs) {
+    this.env = attrs.env;
+    /**
+     * The unique ID of this suite.
+     * @name Suite#id
+     * @readonly
+     * @type {string}
+     * @since 2.0.0
+     */
+    this.id = attrs.id;
+    /**
+     * The parent of this suite, or null if this is the top suite.
+     * @name Suite#parentSuite
+     * @readonly
+     * @type {Suite}
+     */
+    this.parentSuite = attrs.parentSuite;
+    /**
+     * The description passed to the {@link describe} that created this suite.
+     * @name Suite#description
+     * @readonly
+     * @type {string}
+     * @since 2.0.0
+     */
+    this.description = attrs.description;
+    this.expectationFactory = attrs.expectationFactory;
+    this.asyncExpectationFactory = attrs.asyncExpectationFactory;
+    this.expectationResultFactory = attrs.expectationResultFactory;
+    this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
+    this.autoCleanClosures =
+      attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
+
+    this.beforeFns = [];
+    this.afterFns = [];
+    this.beforeAllFns = [];
+    this.afterAllFns = [];
+
+    this.timer = attrs.timer || new j$.Timer();
+
+    /**
+     * The suite's children.
+     * @name Suite#children
+     * @type {Array.<(Spec|Suite)>}
+     * @since 2.0.0
+     */
+    this.children = [];
+
+    this.reset();
+  }
+
+  Suite.prototype.setSuiteProperty = function(key, value) {
+    this.result.properties = this.result.properties || {};
+    this.result.properties[key] = value;
+  };
+
+  Suite.prototype.expect = function(actual) {
+    return this.expectationFactory(actual, this);
+  };
+
+  Suite.prototype.expectAsync = function(actual) {
+    return this.asyncExpectationFactory(actual, this);
+  };
+
+  /**
+   * The full description including all ancestors of this suite.
+   * @name Suite#getFullName
+   * @function
+   * @returns {string}
+   * @since 2.0.0
+   */
+  Suite.prototype.getFullName = function() {
+    var fullName = [];
+    for (
+      var parentSuite = this;
+      parentSuite;
+      parentSuite = parentSuite.parentSuite
+    ) {
+      if (parentSuite.parentSuite) {
+        fullName.unshift(parentSuite.description);
+      }
+    }
+    return fullName.join(' ');
+  };
+
+  /*
+   * Mark the suite with "pending" status
+   */
+  Suite.prototype.pend = function() {
+    this.markedPending = true;
+  };
+
+  /*
+   * Like {@link Suite#pend}, but pending state will survive {@link Spec#reset}
+   * Useful for fdescribe, xdescribe, where pending state should remain.
+   */
+  Suite.prototype.exclude = function() {
+    this.pend();
+    this.markedExcluding = true;
+  };
+
+  Suite.prototype.beforeEach = function(fn) {
+    this.beforeFns.unshift(fn);
+  };
+
+  Suite.prototype.beforeAll = function(fn) {
+    this.beforeAllFns.push(fn);
+  };
+
+  Suite.prototype.afterEach = function(fn) {
+    this.afterFns.unshift(fn);
+  };
+
+  Suite.prototype.afterAll = function(fn) {
+    this.afterAllFns.unshift(fn);
+  };
+
+  Suite.prototype.startTimer = function() {
+    this.timer.start();
+  };
+
+  Suite.prototype.endTimer = function() {
+    this.result.duration = this.timer.elapsed();
+  };
+
+  function removeFns(queueableFns) {
+    for (var i = 0; i < queueableFns.length; i++) {
+      queueableFns[i].fn = null;
+    }
+  }
+
+  Suite.prototype.cleanupBeforeAfter = function() {
+    if (this.autoCleanClosures) {
+      removeFns(this.beforeAllFns);
+      removeFns(this.afterAllFns);
+      removeFns(this.beforeFns);
+      removeFns(this.afterFns);
+    }
+  };
+
+  Suite.prototype.reset = function() {
+    /**
+     * @typedef SuiteResult
+     * @property {Int} id - The unique id of this suite.
+     * @property {String} description - The description text passed to the {@link describe} that made this suite.
+     * @property {String} fullName - The full description including all ancestors of this suite.
+     * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
+     * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite.
+     * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite.
+     * @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach.
+     * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty}
+     * @since 2.0.0
+     */
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: [],
+      deprecationWarnings: [],
+      duration: null,
+      properties: null
+    };
+    this.markedPending = this.markedExcluding;
+    this.children.forEach(function(child) {
+      child.reset();
+    });
+  };
+
+  Suite.prototype.addChild = function(child) {
+    this.children.push(child);
+  };
+
+  Suite.prototype.status = function() {
+    if (this.markedPending) {
+      return 'pending';
+    }
+
+    if (this.result.failedExpectations.length > 0) {
+      return 'failed';
+    } else {
+      return 'passed';
+    }
+  };
+
+  Suite.prototype.canBeReentered = function() {
+    return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0;
+  };
+
+  Suite.prototype.getResult = function() {
+    this.result.status = this.status();
+    return this.result;
+  };
+
+  Suite.prototype.sharedUserContext = function() {
+    if (!this.sharedContext) {
+      this.sharedContext = this.parentSuite
+        ? this.parentSuite.clonedSharedUserContext()
+        : new j$.UserContext();
+    }
+
+    return this.sharedContext;
+  };
+
+  Suite.prototype.clonedSharedUserContext = function() {
+    return j$.UserContext.fromExisting(this.sharedUserContext());
+  };
+
+  Suite.prototype.onException = function() {
+    if (arguments[0] instanceof j$.errors.ExpectationFailed) {
+      return;
+    }
+
+    var data = {
+      matcherName: '',
+      passed: false,
+      expected: '',
+      actual: '',
+      error: arguments[0]
+    };
+    var failedExpectation = this.expectationResultFactory(data);
+
+    if (!this.parentSuite) {
+      failedExpectation.globalErrorType = 'afterAll';
+    }
+
+    this.result.failedExpectations.push(failedExpectation);
+  };
+
+  Suite.prototype.onMultipleDone = function() {
+    var msg;
+
+    // Issue a deprecation. Include the context ourselves and pass
+    // ignoreRunnable: true, since getting here always means that we've already
+    // moved on and the current runnable isn't the one that caused the problem.
+    if (this.parentSuite) {
+      msg =
+        "An asynchronous function called its 'done' callback more than " +
+        'once. This is a bug in the spec, beforeAll, beforeEach, afterAll, ' +
+        'or afterEach function in question. This will be treated as an error ' +
+        'in a future version. See' +
+        '<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> ' +
+        'for more information.\n' +
+        '(in suite: ' +
+        this.getFullName() +
+        ')';
+    } else {
+      msg =
+        'A top-level beforeAll or afterAll function called its ' +
+        "'done' callback more than once. This is a bug in the beforeAll " +
+        'or afterAll function in question. This will be treated as an ' +
+        'error in a future version. See' +
+        '<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> ' +
+        'for more information.';
+    }
+
+    this.env.deprecated(msg, { ignoreRunnable: true });
+  };
+
+  Suite.prototype.addExpectationResult = function() {
+    if (isFailure(arguments)) {
+      var data = arguments[1];
+      this.result.failedExpectations.push(this.expectationResultFactory(data));
+      if (this.throwOnExpectationFailure) {
+        throw new j$.errors.ExpectationFailed();
+      }
+    }
+  };
+
+  Suite.prototype.addDeprecationWarning = function(deprecation) {
+    if (typeof deprecation === 'string') {
+      deprecation = { message: deprecation };
+    }
+    this.result.deprecationWarnings.push(
+      this.expectationResultFactory(deprecation)
+    );
+  };
+
+  function isFailure(args) {
+    return !args[0];
+  }
+
+  return Suite;
+};
+
+if (typeof window == void 0 && typeof exports == 'object') {
+  /* globals exports */
+  exports.Suite = jasmineRequire.Suite;
+}
+
+getJasmineRequireObj().Timer = function() {
+  var defaultNow = (function(Date) {
+    return function() {
+      return new Date().getTime();
+    };
+  })(Date);
+
+  function Timer(options) {
+    options = options || {};
+
+    var now = options.now || defaultNow,
+      startTime;
+
+    this.start = function() {
+      startTime = now();
+    };
+
+    this.elapsed = function() {
+      return now() - startTime;
+    };
+  }
+
+  return Timer;
+};
+
+getJasmineRequireObj().TreeProcessor = function() {
+  function TreeProcessor(attrs) {
+    var tree = attrs.tree,
+      runnableIds = attrs.runnableIds,
+      queueRunnerFactory = attrs.queueRunnerFactory,
+      nodeStart = attrs.nodeStart || function() {},
+      nodeComplete = attrs.nodeComplete || function() {},
+      failSpecWithNoExpectations = !!attrs.failSpecWithNoExpectations,
+      orderChildren =
+        attrs.orderChildren ||
+        function(node) {
+          return node.children;
+        },
+      excludeNode =
+        attrs.excludeNode ||
+        function(node) {
+          return false;
+        },
+      stats = { valid: true },
+      processed = false,
+      defaultMin = Infinity,
+      defaultMax = 1 - Infinity;
+
+    this.processTree = function() {
+      processNode(tree, true);
+      processed = true;
+      return stats;
+    };
+
+    this.execute = function(done) {
+      if (!processed) {
+        this.processTree();
+      }
+
+      if (!stats.valid) {
+        throw 'invalid order';
+      }
+
+      var childFns = wrapChildren(tree, 0);
+
+      queueRunnerFactory({
+        queueableFns: childFns,
+        userContext: tree.sharedUserContext(),
+        onException: function() {
+          tree.onException.apply(tree, arguments);
+        },
+        onComplete: done,
+        onMultipleDone: tree.onMultipleDone
+          ? tree.onMultipleDone.bind(tree)
+          : null
+      });
+    };
+
+    function runnableIndex(id) {
+      for (var i = 0; i < runnableIds.length; i++) {
+        if (runnableIds[i] === id) {
+          return i;
+        }
+      }
+    }
+
+    function processNode(node, parentExcluded) {
+      var executableIndex = runnableIndex(node.id);
+
+      if (executableIndex !== undefined) {
+        parentExcluded = false;
+      }
+
+      if (!node.children) {
+        var excluded = parentExcluded || excludeNode(node);
+        stats[node.id] = {
+          excluded: excluded,
+          willExecute: !excluded && !node.markedPending,
+          segments: [
+            {
+              index: 0,
+              owner: node,
+              nodes: [node],
+              min: startingMin(executableIndex),
+              max: startingMax(executableIndex)
+            }
+          ]
+        };
+      } else {
+        var hasExecutableChild = false;
+
+        var orderedChildren = orderChildren(node);
+
+        for (var i = 0; i < orderedChildren.length; i++) {
+          var child = orderedChildren[i];
+
+          processNode(child, parentExcluded);
+
+          if (!stats.valid) {
+            return;
+          }
+
+          var childStats = stats[child.id];
+
+          hasExecutableChild = hasExecutableChild || childStats.willExecute;
+        }
+
+        stats[node.id] = {
+          excluded: parentExcluded,
+          willExecute: hasExecutableChild
+        };
+
+        segmentChildren(node, orderedChildren, stats[node.id], executableIndex);
+
+        if (!node.canBeReentered() && stats[node.id].segments.length > 1) {
+          stats = { valid: false };
+        }
+      }
+    }
+
+    function startingMin(executableIndex) {
+      return executableIndex === undefined ? defaultMin : executableIndex;
+    }
+
+    function startingMax(executableIndex) {
+      return executableIndex === undefined ? defaultMax : executableIndex;
+    }
+
+    function segmentChildren(
+      node,
+      orderedChildren,
+      nodeStats,
+      executableIndex
+    ) {
+      var currentSegment = {
+          index: 0,
+          owner: node,
+          nodes: [],
+          min: startingMin(executableIndex),
+          max: startingMax(executableIndex)
+        },
+        result = [currentSegment],
+        lastMax = defaultMax,
+        orderedChildSegments = orderChildSegments(orderedChildren);
+
+      function isSegmentBoundary(minIndex) {
+        return (
+          lastMax !== defaultMax &&
+          minIndex !== defaultMin &&
+          lastMax < minIndex - 1
+        );
+      }
+
+      for (var i = 0; i < orderedChildSegments.length; i++) {
+        var childSegment = orderedChildSegments[i],
+          maxIndex = childSegment.max,
+          minIndex = childSegment.min;
+
+        if (isSegmentBoundary(minIndex)) {
+          currentSegment = {
+            index: result.length,
+            owner: node,
+            nodes: [],
+            min: defaultMin,
+            max: defaultMax
+          };
+          result.push(currentSegment);
+        }
+
+        currentSegment.nodes.push(childSegment);
+        currentSegment.min = Math.min(currentSegment.min, minIndex);
+        currentSegment.max = Math.max(currentSegment.max, maxIndex);
+        lastMax = maxIndex;
+      }
+
+      nodeStats.segments = result;
+    }
+
+    function orderChildSegments(children) {
+      var specifiedOrder = [],
+        unspecifiedOrder = [];
+
+      for (var i = 0; i < children.length; i++) {
+        var child = children[i],
+          segments = stats[child.id].segments;
+
+        for (var j = 0; j < segments.length; j++) {
+          var seg = segments[j];
+
+          if (seg.min === defaultMin) {
+            unspecifiedOrder.push(seg);
+          } else {
+            specifiedOrder.push(seg);
+          }
+        }
+      }
+
+      specifiedOrder.sort(function(a, b) {
+        return a.min - b.min;
+      });
+
+      return specifiedOrder.concat(unspecifiedOrder);
+    }
+
+    function executeNode(node, segmentNumber) {
+      if (node.children) {
+        return {
+          fn: function(done) {
+            var onStart = {
+              fn: function(next) {
+                nodeStart(node, next);
+              }
+            };
+
+            queueRunnerFactory({
+              onComplete: function() {
+                var args = Array.prototype.slice.call(arguments, [0]);
+                node.cleanupBeforeAfter();
+                nodeComplete(node, node.getResult(), function() {
+                  done.apply(undefined, args);
+                });
+              },
+              queueableFns: [onStart].concat(wrapChildren(node, segmentNumber)),
+              userContext: node.sharedUserContext(),
+              onException: function() {
+                node.onException.apply(node, arguments);
+              },
+              onMultipleDone: node.onMultipleDone
+                ? node.onMultipleDone.bind(node)
+                : null
+            });
+          }
+        };
+      } else {
+        return {
+          fn: function(done) {
+            node.execute(
+              done,
+              stats[node.id].excluded,
+              failSpecWithNoExpectations
+            );
+          }
+        };
+      }
+    }
+
+    function wrapChildren(node, segmentNumber) {
+      var result = [],
+        segmentChildren = stats[node.id].segments[segmentNumber].nodes;
+
+      for (var i = 0; i < segmentChildren.length; i++) {
+        result.push(
+          executeNode(segmentChildren[i].owner, segmentChildren[i].index)
+        );
+      }
+
+      if (!stats[node.id].willExecute) {
+        return result;
+      }
+
+      return node.beforeAllFns.concat(result).concat(node.afterAllFns);
+    }
+  }
+
+  return TreeProcessor;
+};
+
+getJasmineRequireObj().UserContext = function(j$) {
+  function UserContext() {}
+
+  UserContext.fromExisting = function(oldContext) {
+    var context = new UserContext();
+
+    for (var prop in oldContext) {
+      if (oldContext.hasOwnProperty(prop)) {
+        context[prop] = oldContext[prop];
+      }
+    }
+
+    return context;
+  };
+
+  return UserContext;
+};
+
+getJasmineRequireObj().version = function() {
+  return '3.99.1';
+};
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/lib/update-jasmine.sh
@@ -0,0 +1,5 @@
+# This script is used to build our custom version of the Jasmine library by
+# downloading the upstream file and applying our patches, and is meant for use
+# only when upgrading to a new upstream version.
+curl -o jasmine.js https://raw.githubusercontent.com/jasmine/jasmine/v3.99.1/lib/jasmine-core/jasmine.js
+# patch <local-modifications.patch jasmine.js
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/meson.build
@@ -0,0 +1,100 @@
+project('jasmine-gjs', version: '3.99.1', license: 'MIT',
+    meson_version: '>= 0.58.0')
+
+gjs_dep = dependency('gjs-1.0', required: false)
+if gjs_dep.found()
+    gjs = find_program(gjs_dep.get_variable('gjs_console'))
+else
+    gjs = find_program('gjs', 'gjs-console')
+endif
+
+pkglibexecdir = join_paths(get_option('libexecdir'), meson.project_name())
+pkgdatadir = join_paths(get_option('datadir'), meson.project_name())
+jasmine_mod = meson.project_name()
+
+uninstalled_pkglibexecdir = meson.current_build_dir()
+uninstalled_pkgdatadir = join_paths(meson.current_source_dir(), 'src')
+uninstalled_jasmine_mod = 'lib'
+
+# Executables
+
+config = configuration_data()
+if meson.is_subproject()
+    config.set('pkgdatadir', uninstalled_pkgdatadir)
+    config.set('pkglibexecdir', uninstalled_pkglibexecdir)
+    config.set('jasmine_mod', uninstalled_jasmine_mod)
+else
+    config.set('pkgdatadir', join_paths(get_option('prefix'), pkgdatadir))
+    config.set('pkglibexecdir', join_paths(get_option('prefix'), pkglibexecdir))
+    config.set('jasmine_mod', jasmine_mod)
+endif
+config.set('PACKAGE_VERSION', meson.project_version())
+
+jasmine = configure_file(configuration: config, input: 'bin/jasmine.in',
+    output: 'jasmine', install: not meson.is_subproject(), install_dir: 'bin')
+
+configure_file(configuration: config,
+    input: 'bin/jasmine-runner.in', output: 'jasmine-runner',
+    install: not meson.is_subproject(),
+    install_dir: pkglibexecdir)
+
+meson.override_find_program('jasmine', jasmine)
+
+# Source code and Jasmine library
+
+if not meson.is_subproject()
+    install_data(
+        'lib/jasmine.js',
+        'src/command.js',
+        'src/config.js',
+        'src/consoleReporter.js',
+        'src/jasmineBoot.js',
+        'src/junitReporter.js',
+        'src/options.js',
+        'src/tapReporter.js',
+        'src/timer.js',
+        'src/utils.js',
+        'src/verboseReporter.js',
+        'src/xmlWriter.js',
+        install_dir: pkgdatadir,
+    )
+endif
+
+# Documentation
+
+if not meson.is_subproject()
+    install_data('jasmine.man', rename: 'jasmine.1',
+        install_dir: join_paths(get_option('datadir'), 'man', 'man1'))
+endif
+
+# Tests
+
+tests = [
+    '0_your_first_suite',
+    'custom_matcher',
+
+    'commandSpec',
+    'configSpec',
+    'consoleReporterSpec',
+    'defaultReporterSpec',
+    'importerSpec',
+    'jasmineBootSpec',
+    'junitReporterSpec',
+    'optionsSpec',
+    'tapReporterSpec',
+    'timerSpec',
+    'utilsSpec',
+    'verboseReporterSpec',
+    'xmlWriterSpec',
+]
+if not meson.is_subproject()
+    test_env = environment()
+    test_env.set('TEST_PKGDATADIR', uninstalled_pkgdatadir)
+    test_env.set('TEST_PKGLIBEXECDIR', uninstalled_pkglibexecdir)
+    test_env.set('TEST_JASMINE_MOD', uninstalled_jasmine_mod)
+    foreach t : tests
+        test_file = files('test/@0@.js'.format(t))
+        test(t, gjs, args: ['-m', jasmine, test_file, '--module', '--tap', '--no-config'],
+            env: test_env, protocol: 'tap')
+    endforeach
+endif
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/release-checklist.md
@@ -0,0 +1,47 @@
+# Release Checklist #
+
+## Updating to a new upstream release ##
+
+- [ ] Bump version in `meson.build`.
+- [ ] Change upstream Jasmine version in the `lib/update-jasmine.sh` script.
+- [ ] Delete `lib/jasmine.js`.
+- [ ] Run `cd lib; ./update-jasmine.sh` to regenerate `lib/jasmine.js` from the new upstream version.
+- [ ] Resolve patch conflicts with the new upstream version if necessary.
+- [ ] If making a new major release, update `test/jasmineIntegrationTest.js` and `test/focusedSpecIntegrationTest.js` from the appropriate point in the history of `https://github.com/jasmine/jasmine.github.io/tree/d9b48e17/_tutorials/src`.
+      Currently we use `0_your_first_suite.js` and `custom_matcher.js`, but there may be new features that should be included as well.
+      Remove the browser feature detection and put the "long asynchronous suite" test inside a check for an environment variable.
+- [ ] If making a new major release, see if there are any new features that we can use to improve jasmine-gjs's own tests.
+- [ ] Run `meson test`.
+- [ ] Make a commit.
+- [ ] Write release notes in `NEWS.md`.
+
+## Packaging: RPM ##
+
+- [ ] Update the version number in `jasmine-gjs.spec`.
+- [ ] Add a changelog entry in `jasmine-gjs.spec`.
+- [ ] Make any other necessary changes to `jasmine-gjs.spec`.
+- [ ] Run `ninja dist`.
+- [ ] Copy `jasmine-gjs-<VERSION>.tar.xz` to `rpmbuild/SOURCES`.
+- [ ] Copy `jasmine-gjs.spec` to `rpmbuild/SPECS`.
+- [ ] Run `rpmbuild -ba /path/to/rpmbuild/SPECS/jasmine-gjs.spec` to ensure that everything builds OK.
+- [ ] Make a commit.
+
+## Packaging: Debian ##
+
+- [ ] Update the changelog with `dch`. (Distribution = `unstable`)
+- [ ] Make any other necessary changes to the `debian` directory.
+- [ ] Run `debuild -uc -us -b` to ensure that everything builds OK.
+- [ ] Make a commit.
+- [ ] Run `ninja dist`.
+- [ ] Copy `jasmine-gjs-<VERSION>.tar.xz` to `../jasmine-gjs_<VERSION>.orig.tar.xz`.
+- [ ] Extract the tarball.
+- [ ] Enter the directory and run `debuild -uc -us -S` to build a source package.
+
+## Releasing ##
+
+- [ ] Run `git tag <VERSION>`; version is just dotted numbers, e.g. `2.2.1`, no `v` or `Version`.
+- [ ] Push the tag to GitHub.
+- [ ] Copy the release notes from `NEWS.md` into https://github.com/ptomato/jasmine-gjs/releases
+- [ ] Attach the tarball and the sha256sum from the `meson-dist` directory to the release notes.
+- [ ] Attach the RPM packages from `/path/to/rpmbuild/RPMS/` and `/path/to/rpmbuild/SRPMS` to the release notes.
+- [ ] Attach the Debian packages (`jasmine-gjs_<VERSION>-1_all.deb`, `jasmine-gjs_<VERSION>.orig.tar.xz`, `jasmine-gjs_<VERSION>-1.debian.tar.xz`, and `jasmine-gjs_<VERSION>-1.dsc`) to the release notes.
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/command.js
@@ -0,0 +1,113 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import * as Options from './options.js';
+import * as Timer from './timer.js';
+import * as JUnitReporter from './junitReporter.js';
+import * as VerboseReporter from './verboseReporter.js';
+import * as TapReporter from './tapReporter.js';
+import * as ConsoleReporter from './consoleReporter.js';
+
+export const mainloop = GLib.MainLoop.new(null, false);
+
+export async function run(_jasmine, argv, timeout = -1) {
+    const [files, options] = Options.parseOptions(argv);
+
+    if (options.exclude)
+        _jasmine.exclusions = options.exclude;
+
+    if (options.module)
+        _jasmine.module = true;
+
+    if (options.junit) {
+        let junitPath = options.junit;
+        if (!GLib.path_is_absolute(junitPath) &&
+            GLib.getenv('JASMINE_JUNIT_REPORTS_DIR') !== null)
+            junitPath = `${GLib.getenv('JASMINE_JUNIT_REPORTS_DIR')}/${junitPath}`;
+        const junitFile = Gio.File.new_for_commandline_arg(junitPath);
+
+        // Since people might want their report dir structure to mirror
+        // their test dir structure, we shall be kind and try to create any
+        // report directories that don't exist.
+        try {
+            junitFile.get_parent().make_directory_with_parents(null);
+        } catch (e) {
+            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
+                throw e;
+            // ignore error if directory already exists
+        }
+
+        const rawStream = junitFile.replace(null, false, Gio.FileCreateFlags.NONE, null);
+        const junitStream = new Gio.DataOutputStream({
+            base_stream: rawStream,
+        });
+
+        const junitReporter = new JUnitReporter.JUnitReporter({
+            timerFactory: Timer.createDefaultTimer,
+            print(str) {
+                junitStream.put_string(str, null);
+            },
+        });
+        junitReporter.connect('complete', () => junitStream.close(null));
+        _jasmine.addReporter(junitReporter);
+    }
+
+    let timeoutId;
+    const reporterOptions = {
+        show_colors: options.color,
+        timerFactory: Timer.createDefaultTimer,
+    };
+    let exitCode = 0;
+
+    let reporter;
+    if (options.verbose)
+        reporter = new VerboseReporter.VerboseReporter(reporterOptions);
+    else if (options.tap)
+        reporter = new TapReporter.TapReporter(reporterOptions);
+    else
+        reporter = new ConsoleReporter.DefaultReporter(reporterOptions);
+
+    reporter.connect('started', () => GLib.source_remove(timeoutId));
+    reporter.connect('complete', (_, success) => {
+        if (!success)
+            exitCode = 1;
+        mainloop.quit();
+    });
+    _jasmine.addReporter(reporter);
+
+    // This works around a limitation in GJS 1.40 where exceptions occurring
+    // during module import are swallowed.
+    if (timeout !== -1) {
+        timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, timeout, function () {
+            if (options.tap)
+                print('Bail out! Test suite failed to start within 10 seconds');
+            else
+                printerr('Test suite failed to start within 10 seconds');
+            exitCode = 1;
+            mainloop.quit();
+        });
+    }
+
+    // This should start after the main loop starts, otherwise we will hit
+    // Mainloop.run() only after several tests have already run. For consistency
+    // we should guarantee that there is a main loop running during the tests.
+    GLib.idle_add(GLib.PRIORITY_DEFAULT, function () {
+        _jasmine.execute(files).catch(e => {
+            if (options.tap) {
+                // "Bail out!" has a special meaning to TAP harnesses
+                print('Bail out! Exception occurred inside Jasmine:', e);
+            } else {
+                printerr('Exception occurred inside Jasmine:');
+                printerr(e);
+                printerr(e.stack);
+            }
+            exitCode = 1;
+            mainloop.quit();
+        });
+        return GLib.SOURCE_REMOVE;
+    });
+
+    // _jasmine.execute() queues up all the tests and runs them asynchronously.
+    await mainloop.runAsync();
+    return exitCode;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/config.js
@@ -0,0 +1,142 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+function _makePathsAbsolute(configFile, paths) {
+    return paths.map(path => {
+        if (GLib.path_is_absolute(path))
+            return path;
+        return configFile.get_parent().resolve_relative_path(path).get_path();
+    });
+}
+
+// Make it legal to specify "some_option": "single_value" in the config file as
+// well as "some_option": ["multiple", "values"]
+export function ensureArray(option) {
+    if (!Array.isArray(option))
+        return [option];
+    return option;
+}
+
+export function loadConfig(options, defaultFile = 'jasmine.json') {
+    if (options['no-config'])
+        return {};
+
+    let config = {};
+    const configFile = Gio.File.new_for_commandline_arg(options.config || defaultFile);
+
+    try {
+        let [, contents] = configFile.load_contents(null);
+        if (contents instanceof Uint8Array)
+            contents = imports.byteArray.toString(contents);
+        config = JSON.parse(contents);
+    } catch (e) {
+        if (!options.config && e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
+            return {};  // Don't complain if config file absent from default location
+        throw new Error(`Configuration not read from ${configFile.get_path()}`);
+    }
+
+    if (config.include_paths) {
+        config.include_paths = _makePathsAbsolute(configFile,
+            ensureArray(config.include_paths));
+    }
+    if (config.spec_files) {
+        config.spec_files = _makePathsAbsolute(configFile,
+            ensureArray(config.spec_files));
+    }
+
+    const RECOGNIZED_KEYS = [
+        'environment',
+        'exclude',
+        'include_paths',
+        'interpreter',
+        'options',
+        'spec_files',
+    ];
+    Object.keys(config).forEach(key => {
+        if (RECOGNIZED_KEYS.indexOf(key) === -1)
+            printerr(`warning: unrecognized config file key "${key}"`);
+    });
+
+    print('Configuration loaded from', configFile.get_path());
+    return config;
+}
+
+function optionsToArgs(options) {
+    const args = [options.color ? '--color' : '--no-color'];
+    if (options.verbose)
+        args.push('--verbose');
+    if (options.module)
+        args.push('--module');
+    if (options.tap)
+        args.push('--tap');
+    if (options.junit) {
+        args.push('--junit');
+        args.push(options.junit);
+    }
+    if (options.exclude) {
+        ensureArray(options.exclude).forEach(exclude => {
+            args.push('--exclude');
+            args.push(exclude);
+        });
+    }
+    return args;
+}
+
+export function configToArgs(config, specFiles = [], options = {}) {
+    let retval = [];
+    if (config.exclude) {
+        ensureArray(config.exclude).forEach(exclude => {
+            retval.push('--exclude');
+            retval.push(exclude);
+        });
+    }
+
+    // Command-line options should always override config file options
+    if (config.options)
+        retval = retval.concat(ensureArray(config.options));
+    retval = retval.concat(optionsToArgs(options), specFiles);
+    // Specific tests given on the command line should always override the
+    // default tests in the config file
+    if (specFiles.length === 0 && config.spec_files)
+        retval = retval.concat(ensureArray(config.spec_files));
+
+    return retval;
+}
+
+export function prepareLauncher(config, options = {}) {
+    let flags = Gio.SubprocessFlags.NONE;
+    if (options.debug)
+        flags |= Gio.SubprocessFlags.STDIN_INHERIT;
+    const launcher = new Gio.SubprocessLauncher({flags});
+    if (config.environment) {
+        Object.keys(config.environment).forEach(key => {
+            if (config.environment[key] === null)
+                launcher.unsetenv(key);
+            else
+                launcher.setenv(key, config.environment[key], true);
+        });
+    }
+    if (config.include_paths) {
+        const existingPaths = launcher.getenv('GJS_PATH');
+        const paths = ensureArray(config.include_paths).slice();
+        if (existingPaths)
+            paths.unshift(existingPaths);
+        launcher.setenv('GJS_PATH', paths.join(':'), /* overwrite = */ true);
+    }
+    return launcher;
+}
+
+export function wrapArgs(args, config, options = {}) {
+    if (options.interpreter)
+        args.unshift(...options.interpreter.split(' '));
+    else if (config.interpreter)
+        args.unshift(...config.interpreter.split(' '));
+    else if (options.module)
+        args.push('--module');
+    if (options.debug) {
+        if (!options.interpreter && !config.interpreter)
+            args.unshift('gjs', '-m');
+        args.unshift(...options.debug.split(' '));
+    }
+    return args;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/consoleReporter.js
@@ -0,0 +1,262 @@
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+
+import {indenter} from './utils.js';
+
+const YELLOW = '\x1b[33m';
+const GREEN = '\x1b[32m';
+const RED = '\x1b[31m';
+const NORMAL = '\x1b[0m';
+
+let GioUnix = null;
+try {
+    GioUnix = (await import('gi://GioUnix')).default;
+} catch (e) {}
+
+function createNoopTimer() {
+    return {
+        start() {},
+        elapsed() {
+            return 0;
+        },
+    };
+}
+
+export const ConsoleReporter = GObject.registerClass({
+    Properties: {
+        'show-colors': GObject.ParamSpec.boolean('show-colors', 'Show colors',
+            'Whether to print color output',
+            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+            true),
+        'jasmine-core-path': GObject.ParamSpec.string('jasmine-core-path',
+            'Jasmine core path',
+            'Path to Jasmine core module for stack trace purposes',
+            GObject.ParamFlags.READWRITE,
+            '/nowhere'),
+    },
+
+    Signals: {
+        'started': {},
+        'complete': {
+            param_types: [GObject.TYPE_BOOLEAN],
+        },
+    },
+}, class ConsoleReporter extends GObject.Object {
+    _init(props = {}) {
+        if (props.hasOwnProperty('print')) {
+            this._print = props.print;
+            delete props.print;
+        }
+
+        this._timerFactory = createNoopTimer;
+        if (props.hasOwnProperty('timerFactory')) {
+            this._timerFactory = props.timerFactory;
+            delete props.timerFactory;
+        }
+
+        super._init(props);
+
+        // The main timer should return 0 if the run hasn't started yet
+        this._timers = {
+            'main': createNoopTimer(),
+        };
+
+        this._failedSpecs = [];
+        this._failedSuites = [];
+        this._suiteLevel = 0;
+        this._specCount = 0;
+        this._passingCount = 0;
+        this._failureCount = 0;
+        this._pendingCount = 0;
+    }
+
+    _color(str, color) {
+        if (typeof color !== 'undefined')
+            return this.show_colors ? color + str + NORMAL : str;
+        return str;
+    }
+
+    // default print function that prints to stdout (GJS' built-in print
+    // functions, print() and printerr(), unfortunately append newlines to
+    // everything)
+    static getStdout() {
+        if (!this._stdout) {
+            const UnixOutputStream = GioUnix
+                ? GioUnix.OutputStream
+                : Gio.UnixOutputStream;
+            const FD_STDOUT = 1;
+            const fdstream = new UnixOutputStream({
+                fd: FD_STDOUT,
+                close_fd: false,
+            });
+            this._stdout = new Gio.DataOutputStream({
+                base_stream: fdstream,
+            });
+        }
+        return this._stdout;
+    }
+
+    _print(str) {
+        ConsoleReporter.getStdout().put_string(str, null);
+    }
+
+    // Used to start a timer associated with a particular ID. Subclasses can use
+    // this to time actions that the base class doesn't time if they wish.
+    startTimer(id) {
+        this._timers[id] = this._timerFactory(id);
+        this._timers[id].start();
+    }
+
+    // Used to get the elapsed time from a timer with the given ID. Subclasses
+    // can use this to time actions not timed by the base class. The base class
+    // uses timers with the following IDs:
+    //   main - times the whole suite
+    //   suite:foo - times the suite with ID "foo"
+    //   spec:bar - times the spec with ID "bar"
+    elapsedTime(id) {
+        return this._timers[id].elapsed();
+    }
+
+    // Called with an "info" object with the following property:
+    //   totalSpecsDefined - number of specs to be run
+    jasmineStarted() {
+        this.emit('started');
+        this.startTimer('main');
+    }
+
+    jasmineDone() {
+        this.elapsedTime('main');  // Stop the timer
+        this.emit('complete', this._failureCount === 0);
+    }
+
+    // Called with a "result" object with the following properties:
+    //   id - a string unique to this suite
+    //   description - the name of the suite passed to describe()
+    //   fullName - the full name including the names of parent suites
+    //   failedExpectations - a list of failures in this suite
+    suiteStarted(result) {
+        if (result.id)
+            this.startTimer(`suite:${result.id}`);
+        this._suiteLevel++;
+    }
+
+    // Called with the same object as suiteStarted(), with an extra property:
+    //   status - "disabled", "failed", or "finished"
+    // Adds another extra property if the suite was started properly with ID:
+    //   time - time taken to execute the suite, in milliseconds
+    suiteDone(result) {
+        this._suiteLevel--;
+        if (result.failedExpectations && result.failedExpectations.length > 0) {
+            this._failureCount++;
+            this._failedSuites.push(result);
+        }
+        if (result.id)
+            result.time = this.elapsedTime(`suite:${result.id}`);
+    }
+
+    // Called with a "result" object with the following properties:
+    //   id - a string unique to this spec
+    //   description: the name of the spec passed to it()
+    //   fullName - the full name concatenated with the suite's full name
+    //   failedExpectations - a list of failures in this spec
+    //   passedExpectations - a list of succeeded expectations in this spec
+    specStarted(result) {
+        if (result.id)
+            this.startTimer(`spec:${result.id}`);
+        this._specCount++;
+    }
+
+    // Called with the same object as specStarted(), with an extra property:
+    //   status - "disabled", "pending", "failed", or "passed"
+    // Adds another extra property if the spec was started properly with ID:
+    //   time - time taken to execute the spec, in milliseconds
+    specDone(result) {
+        if (result.status === 'passed') {
+            this._passingCount++;
+        } else if (result.status === 'pending') {
+            this._pendingCount++;
+        } else if (result.status === 'failed') {
+            this._failureCount++;
+            this._failedSpecs.push(result);
+        }
+        if (result.id)
+            result.time = this.elapsedTime(`spec:${result.id}`);
+    }
+
+    filterStack(stack) {
+        return stack.split('\n').filter(stackLine => {
+            return stackLine.indexOf(this.jasmine_core_path) === -1;
+        }).join('\n');
+    }
+});
+
+// This reporter has very nearly the same behaviour to Jasmine's default console
+// reporter.
+export const DefaultReporter = GObject.registerClass(class DefaultReporter extends ConsoleReporter {
+    jasmineStarted(info) {
+        super.jasmineStarted(info);
+        this._print('Started\n');
+    }
+
+    jasmineDone() {
+        this._print('\n\n');
+        if (this._failedSpecs.length > 0)
+            this._print('Failures:');
+        this._failedSpecs.forEach(this._printSpecFailureDetails, this);
+
+        if (this._specCount > 0) {
+            this._print('\n');
+            this._print(`${this._specCount} spec${this._specCount === 1 ? '' : 's'}, ${this._failureCount} failed`);
+
+            if (this._pendingCount)
+                this._print(`, ${this._pendingCount} pending`);
+        } else {
+            this._print('No specs found');
+        }
+
+        this._print('\n');
+        const seconds = Math.round(this.elapsedTime('main')) / 1000;
+        this._print(`\nFinished in ${seconds} s\n`);
+
+        this._failedSuites.forEach(this._printSuiteFailureDetails, this);
+
+        super.jasmineDone();
+    }
+
+    specDone(result) {
+        super.specDone(result);
+
+        const colors = {
+            passed: GREEN,
+            pending: YELLOW,
+            failed: RED,
+            disabled: undefined,
+        };
+        const symbols = {
+            passed: '.',
+            pending: '*',
+            failed: 'F',
+            disabled: '',
+        };
+        this._print(this._color(symbols[result.status], colors[result.status]));
+    }
+
+    _printSpecFailureDetails(result, index) {
+        this._print(`\n${index + 1}) ${result.fullName}\n`);
+        result.failedExpectations.forEach(failedExpectation => {
+            const report = `Message:
+${this._color(failedExpectation.message, RED)}
+Stack:
+${this.filterStack(failedExpectation.stack)}
+`;
+            this._print(indenter.indent(report, 2));
+        });
+    }
+
+    _printSuiteFailureDetails(result) {
+        result.failedExpectations.forEach(failedExpectation => {
+            this._print(this._color(`An error was thrown in an afterAll
+AfterAll ${failedExpectation.message}`, RED));
+        });
+    }
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/jasmineBoot.js
@@ -0,0 +1,145 @@
+/* global jasmineImporter */
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+export const Jasmine = class Jasmine {
+    constructor({jasmineCore} = {jasmineCore: jasmineImporter.jasmine}) {
+        const jasmineCorePath = jasmineCore.__file__;
+        this._jasmineCoreFile = Gio.File.new_for_path(jasmineCorePath);
+
+        const jasmineRequire = jasmineCore.getJasmineRequireObj();
+        this._jasmine = jasmineRequire.core(jasmineRequire);
+        this.env = this._jasmine.getEnv();
+        this.env.configure({
+            random: false,
+        });
+        this._jasmineInterface = jasmineRequire.interface(this._jasmine, this.env);
+
+        this.exclusions = [];
+        this.specFiles = [];
+        this.module = false;
+        this._reportersCount = 0;
+    }
+
+    get version() {
+        return this._jasmine.version;
+    }
+
+    addReporter(reporter) {
+        reporter.jasmine_core_path = this._jasmineCoreFile.get_parent().get_path();
+        this.env.addReporter(reporter);
+        this._reportersCount++;
+    }
+
+    _addSpecFile(file) {
+        const absolutePath = file.get_path();
+        const shouldSkip = this.exclusions.some(pattern => {
+            // Match globs against the absolute path
+            if (GLib.pattern_match_simple(pattern, absolutePath))
+                return true;
+            // Also match if the string matches at the end
+            if (GLib.pattern_match_simple(`*/${pattern}`, absolutePath))
+                return true;
+            // Also match if the string matches the path at the end
+            return GLib.pattern_match_simple(`*/${pattern}`,
+                file.get_parent().get_path());
+        });
+        if (shouldSkip)
+            return;
+        if (this.specFiles.indexOf(absolutePath) === -1)
+            this.specFiles.push(absolutePath);
+    }
+
+    addSpecFiles(filePaths) {
+        filePaths.forEach(filePath => {
+            const file = Gio.File.new_for_path(filePath);
+            const type = file.query_file_type(Gio.FileQueryInfoFlags.NONE, null);
+
+            switch (type) {
+            case Gio.FileType.REGULAR:
+            case Gio.FileType.UNKNOWN:
+                this._addSpecFile(file);
+                break;
+            case Gio.FileType.DIRECTORY:
+                recurseDirectory(file, this._addSpecFile.bind(this));
+                break;
+            default:
+                // ignore
+            }
+        });
+    }
+
+    async loadSpecs() {
+        await Promise.all(this.specFiles.map(async file => {
+            const oldSearchPath = imports.searchPath.slice();  // make a copy
+            let specImporter = imports['.'];
+
+            const modulePath = GLib.path_get_dirname(file);
+            const moduleName = GLib.path_get_basename(file).slice(0, -3);  // .js
+
+            // Backwards compatibility - let specs import modules from their own
+            // directories
+            imports.searchPath.unshift(modulePath);
+            specImporter.searchPath.unshift(modulePath);
+
+            try {
+                if (this.module)
+                    await import(`file://${file}`);
+                else
+                    await specImporter[moduleName];
+            } catch (err) {
+                if (!(err instanceof SyntaxError || err.name === 'ImportError'))
+                    throw err;
+                // Fake failing suite, to log a failure but continue on with
+                // other specs
+                globalThis.describe(file, function () {
+                    globalThis.it('did not import correctly', function () {
+                        let failureMessage;
+                        if (err instanceof SyntaxError) {
+                            const {fileName, lineNumber, columnNumber, message} = err;
+                            failureMessage = `${fileName}:${lineNumber}:${columnNumber}: ${message}`;
+                        } else {
+                            failureMessage = err.message;
+                        }
+                        globalThis.fail(failureMessage);
+                    });
+                });
+            }
+            imports.searchPath = oldSearchPath;
+
+            // Make a new copy of the importer in case we need to import another
+            // spec with the same filename, so it is not cached
+            specImporter = specImporter['.'];
+        }));
+    }
+
+    async execute(files) {
+        if (files && files.length > 0)
+            this.addSpecFiles(files);
+
+        await this.loadSpecs();
+        this.env.execute();
+    }
+
+    // Install Jasmine API on the global object
+    installAPI(global) {
+        Object.assign(global, this._jasmineInterface);
+    }
+};
+
+function recurseDirectory(directory, func) {
+    const enumerator = directory.enumerate_children('standard::*',
+        Gio.FileQueryInfoFlags.NONE, null);
+
+    let info;
+    while ((info = enumerator.next_file(null))) {
+        const file = enumerator.get_child(info);
+        const filename = file.get_basename();
+
+        if (info.get_file_type() === Gio.FileType.DIRECTORY)
+            recurseDirectory(file, func);
+        else if (filename.endsWith('.js'))
+            func(file);
+    }
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/junitReporter.js
@@ -0,0 +1,158 @@
+// Reporter that outputs a JUnit XML test report
+// See http://llg.cubic.org/docs/junit/
+// Unfortunately, the JUnit format is woefully underspecified.
+
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+
+import {ConsoleReporter} from './consoleReporter.js';
+import * as XMLWriter from './xmlWriter.js';
+
+export const JUnitReporter = GObject.registerClass(class JUnitReporter extends ConsoleReporter {
+    jasmineStarted(info) {
+        this._currentSuite = null;
+        super.jasmineStarted(info);
+        this._tree = new XMLWriter.Node('testsuites');
+        this._suiteCount = 0;
+        this._activeSuites = [];
+
+        const properties = new XMLWriter.Node('properties');
+        properties.children = GLib.listenv().map(key => {
+            const property = new XMLWriter.Node('property');
+            property.attrs = {
+                name: key,
+                value: GLib.getenv(key),
+            };
+            return property;
+        });
+        this._tree.children.push(properties);
+    }
+
+    jasmineDone() {
+        const failedAfterAlls = this._failedSuites.length;
+        if (failedAfterAlls > 0) {
+            const afterAllSuite = new XMLWriter.Node('testsuite');
+            afterAllSuite.attrs = {
+                name: 'afterAll()',
+                tests: failedAfterAlls,
+                errors: failedAfterAlls,
+                id: this._suiteCount++,
+            };
+            afterAllSuite.children = this._failedSuites.map(failure => {
+                const afterAllCase = new XMLWriter.Node('testcase');
+                afterAllCase.attrs = {
+                    name: failure.description,
+                    classname: 'AfterAll',
+                    assertions: failure.failedExpectations.length,
+                };
+                afterAllCase.children = failure.failedExpectations.map(result => {
+                    const error = new XMLWriter.Node('error');
+                    error.attrs = _parseExceptionMessage(result);
+                    error.text = result.stack;
+                    return error;
+                });
+                return afterAllCase;
+            });
+            this._tree.children.push(afterAllSuite);
+        }
+
+        // Timer inherited from ConsoleReporter
+        this._tree.attrs = {
+            time: this.elapsedTime('main') / 1000,
+        };
+
+        this._print(this._tree.toString());
+
+        super.jasmineDone();
+    }
+
+    // Jenkins parses nested JUnit test suites but doesn't display them properly.
+    // See https://issues.jenkins-ci.org/browse/JENKINS-18673
+    // Therefore, we flatten all suites into one level.
+    suiteStarted(result) {
+        super.suiteStarted(result);
+        this._activeSuites.push(this._currentSuite);
+        this._currentSuite = new XMLWriter.Node('testsuite');
+        this._currentSuite.attrs = {
+            name: result.fullName,
+            id: this._suiteCount++,
+            tests: 0,
+            disabled: 0,
+            failures: 0,
+            errors: 0,
+            skipped: 0,
+            timestamp: GLib.DateTime.new_now_local().format('%Y-%m-%dT%H:%M:%S'),
+        };
+        this._tree.children.push(this._currentSuite);
+    }
+
+    suiteDone(result) {
+        super.suiteDone(result);
+        this._currentSuite.attrs.time = result.time / 1000;  // in seconds
+        this._currentSuite = this._activeSuites.pop();
+    }
+
+    specStarted(result) {
+        super.specStarted(result);
+        this._currentSuite.attrs.tests++;
+    }
+
+    specDone(result) {
+        super.specDone(result);
+
+        const spec = new XMLWriter.Node('testcase');
+        spec.attrs = {
+            name: result.description,
+            classname: this._currentSuite.attrs['name'],
+            assertions: result.failedExpectations.length +
+                result.passedExpectations.length,
+            time: result.time / 1000,  // in seconds
+        };
+
+        switch (result.status) {
+        case 'disabled':
+            this._currentSuite.attrs.disabled++;
+            return;
+        case 'failed': {
+            // We count a failure as a "failure" if at least one expectation
+            // failed. If there were only uncaught exceptions, then it is an
+            // "error".
+            let assertFailed = false;
+            result.failedExpectations.forEach(failedExpectation => {
+                let node;
+                if (failedExpectation.matcherName !== '') {
+                    assertFailed = true;
+                    node = new XMLWriter.Node('failure');
+                    node.attrs = {
+                        type: failedExpectation.matcherName,
+                        message: failedExpectation.message,
+                    };
+                } else {
+                    node = new XMLWriter.Node('error');
+                    node.attrs = _parseExceptionMessage(failedExpectation);
+                }
+                node.text = this.filterStack(failedExpectation.stack);
+                spec.children.push(node);
+            });
+            if (assertFailed)
+                this._currentSuite.attrs.failures++;
+            else
+                this._currentSuite.attrs.errors++;
+        }
+            break;
+        case 'pending':
+            this._currentSuite.attrs.skipped++;
+            spec.children.push(new XMLWriter.Node('skipped'));
+            break;
+        }
+        this._currentSuite.children.push(spec);
+    }
+});
+
+function _parseExceptionMessage(expectation) {
+    const parse = expectation.message.split(':');
+    return {
+        type: parse.length > 1 ? parse[0] : 'Error',
+        message: expectation.message,
+    };
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/options.js
@@ -0,0 +1,150 @@
+import * as System from 'system';
+
+const ARGS = {
+    'help': {
+        help: 'show this help',
+        action: 'help',
+    },
+    'version': {
+        help: 'print program version',
+        action: 'store_true',
+    },
+    'no-color': {
+        help: 'turn off color in spec output',
+        dest: 'color',
+        action: 'store_false',
+        default: true,
+    },
+    'color': {
+        help: 'turn on color in spec output',
+        action: 'store_true',
+    },
+    'verbose': {
+        help: 'print verbose results (similar to mocha)',
+        action: 'store_true',
+    },
+    'tap': {
+        help: 'output results in TAP format',
+        action: 'store_true',
+    },
+    'junit': {
+        help: 'output a JUnit report to the given file [report.xml]',
+        action: 'store',
+        nargs: '?',
+        const: 'report.xml',
+    },
+    'config': {
+        help: 'load configuration from the given file',
+        action: 'store',
+    },
+    'no-config': {
+        help: 'ignore the default jasmine.json config file',
+        action: 'store_true',
+    },
+    'exclude': {
+        help: 'do not execute the given spec (may include wildcards)',
+        action: 'append',
+    },
+    'module': {
+        help: 'execute specs as module',
+        action: 'store_true',
+    },
+    'interpreter': {
+        help: 'run with the given program instead of /usr/bin/env gjs',
+        action: 'store',
+    },
+    'debug': {
+        help: 'run with a debugger [gdb --args]',
+        action: 'store',
+        nargs: '?',
+        const: 'gdb --args',
+    },
+};
+
+export function parseOptions(argv) {
+    argv = argv.slice();  // Make a copy
+    const namespace = {};
+    const files = [];
+
+    Object.keys(ARGS).forEach(function (argName) {
+        const argInfo = ARGS[argName];
+        const dest = argInfo.dest || argName;
+        if (typeof argInfo.default !== 'undefined')
+            namespace[dest] = argInfo.default;
+    });
+
+    let argvElement;
+    while ((argvElement = argv.shift())) {
+        if (!argvElement.startsWith('-')) {
+            files.push(argvElement);
+            continue;
+        }
+
+        if (!argvElement.startsWith('--')) {
+            printerr(`warning: Unknown argument "${argvElement}"`);
+            continue;
+        }
+        const argName = argvElement.slice(2);
+        if (!(argName in ARGS)) {
+            printerr(`warning: Unknown argument "${argvElement}"`);
+            continue;
+        }
+
+        const argInfo = ARGS[argName];
+        const dest = argInfo.dest || argName;
+        let value;
+        switch (argInfo.action) {
+        case 'help':
+            help();
+            break;
+        case 'store_true':
+            namespace[dest] = true;
+            break;
+        case 'store_false':
+            namespace[dest] = false;
+            break;
+        case 'store':
+            value = _getNextArgument(argv);
+            if (typeof value === 'undefined' && argInfo.nargs === '?')
+                value = argInfo.const;
+            if (typeof value === 'undefined') {
+                printerr(`warning: Missing value for argument "${argName}"`);
+                continue;
+            }
+            namespace[dest] = value;
+            break;
+        case 'append':
+            value = _getNextArgument(argv);
+            if (typeof value === 'undefined') {
+                printerr(`warning: Missing value for argument "${argName}"`);
+                continue;
+            }
+            if (!(dest in namespace))
+                namespace[dest] = [];
+            namespace[dest].push(value);
+            break;
+        }
+    }
+    return [files, namespace];
+}
+
+function _getNextArgument(argv) {
+    let value = argv.shift();
+    if (typeof value !== 'undefined' && value.startsWith('--')) {
+        argv.unshift(value);
+        value = undefined;
+    }
+    return value;
+}
+
+function help() {
+    print('Usage: jasmine [-I <include path>] [options] <files or directories>\n');
+    print('If file given, runs the spec in that file. If directory given,');
+    print('searches for and runs specs under that directory.\n');
+    print('Options:');
+    Object.keys(ARGS).forEach(function (argName) {
+        print(`${`--${argName}`.padStart(15)}\t\t${ARGS[argName].help}`);
+    });
+    print('');
+    System.exit(0);
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/tapReporter.js
@@ -0,0 +1,72 @@
+// Reporter that outputs according to the Test Anything Protocol
+// See http://testanything.org/tap-specification.html
+
+import GObject from 'gi://GObject';
+
+import {ConsoleReporter} from './consoleReporter.js';
+
+export const TapReporter = GObject.registerClass(class TapReporter extends ConsoleReporter {
+    jasmineStarted(info) {
+        super.jasmineStarted(info);
+        this._print(`1..${info.totalSpecsDefined}\n`);
+    }
+
+    jasmineDone() {
+        this._failedSuites.forEach(failure => {
+            failure.failedExpectations.forEach(result => {
+                this._print(`not ok - An error was thrown in an afterAll(): ${result.message}\n`);
+            });
+        });
+        super.jasmineDone();
+    }
+
+    suiteStarted(result) {
+        super.suiteStarted(result);
+        this._print(`# Suite started: ${result.fullName}\n`);
+    }
+
+    suiteDone(result) {
+        super.suiteDone(result);
+        if (result.status === 'disabled') {
+            this._print(`# Suite was disabled: ${result.fullName}\n`);
+        } else {
+            const failures = result.failedExpectations.length;
+            this._print(`# Suite finished with ${failures} failure${failures === 1 ? '' : 's'}: ${result.fullName}\n`);
+        }
+    }
+
+    specDone(result) {
+        super.specDone(result);
+
+        if (result.status === 'failed')
+            this._print('not ok');
+        else
+            this._print('ok');
+        this._print(` ${this._specCount} - ${result.fullName}`);
+        if (result.status === 'pending' || result.status === 'disabled') {
+            const reason = result.pendingReason || result.status;
+            this._print(` # SKIP ${reason}`);
+        }
+        if (result.status === 'failed' && result.failedExpectations) {
+            const messages = result.failedExpectations.map(r => _removeNewlines(r.message)).join(' ');
+            this._print(` (${messages})`);
+        }
+        this._print('\n');
+
+        // Print additional diagnostic info on failure
+        if (result.status === 'failed' && result.failedExpectations) {
+            result.failedExpectations.forEach(failedExpectation => {
+                this._print(`# Message: ${_removeNewlines(failedExpectation.message)}\n`);
+                this._print('# Stack:\n');
+                const stackTrace = this.filterStack(failedExpectation.stack).trim();
+                this._print(stackTrace.split('\n').map(str => `#   ${str}`).join('\n'));
+                this._print('\n');
+            });
+        }
+    }
+});
+
+function _removeNewlines(str) {
+    const allNewlines = /\n/g;
+    return str.replace(allNewlines, '\\n');
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/timer.js
@@ -0,0 +1,16 @@
+import GLib from 'gi://GLib';
+
+// Measures elapsed time in milliseconds.
+export function createDefaultTimer() {
+    let startTime, elapsedTime;
+    return {
+        start() {
+            startTime = GLib.get_monotonic_time();
+        },
+        elapsed() {
+            if (!elapsedTime)
+                elapsedTime = (GLib.get_monotonic_time() - startTime) / 1000;
+            return elapsedTime;
+        },
+    };
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/utils.js
@@ -0,0 +1,9 @@
+export const indenter = {
+    indent(str, spaces) {
+        return str.split('\n').map(line => {
+            if (line === '')
+                return line;
+            return ' '.repeat(spaces) + line;
+        }).join('\n');
+    },
+};
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/verboseReporter.js
@@ -0,0 +1,100 @@
+import GObject from 'gi://GObject';
+
+import {ConsoleReporter} from './consoleReporter.js';
+import {indenter} from './utils.js';
+
+const GRAY = '\x1b[38;5;246m';
+const YELLOW = '\x1b[33m';
+const GREEN = '\x1b[32m';
+const RED = '\x1b[31m';
+
+// This reporter, activated with --verbose on the command line, behaves very
+// similarly to Mocha's nicely formatted reporter.
+export const VerboseReporter = GObject.registerClass(class VerboseReporter extends ConsoleReporter {
+    jasmineStarted(info) {
+        super.jasmineStarted(info);
+        this._print('Started\n\n');
+    }
+
+    jasmineDone() {
+        this._print('\n');
+        this._failedSpecs.forEach(this._printSpecFailureDetails, this);
+        const seconds = Math.round(this.elapsedTime('main')) / 1000;
+
+        this._print(this._color(`  ${this._passingCount} passing`, GREEN));
+        this._print(` (${seconds} s)\n`);
+        if (this._pendingCount > 0)
+            this._print(this._color(`  ${this._pendingCount} pending\n`, YELLOW));
+        if (this._failureCount > 0)
+            this._print(this._color(`  ${this._failureCount} failing\n`, RED));
+        this._print('\n');
+
+        this._failedSuites.forEach(this._printSuiteFailureDetails, this);
+
+        super.jasmineDone();
+    }
+
+    suiteStarted(result) {
+        super.suiteStarted(result);
+        this._print(indenter.indent(this._color(result.description, GRAY),
+            this._suiteLevel * 2));
+        this._print('\n');
+    }
+
+    suiteDone(result) {
+        if (result.status === 'disabled') {
+            this._print(indenter.indent(`${this._color('(disabled)', YELLOW)}\n`,
+                this._suiteLevel * 2 + 2));
+        }
+
+        super.suiteDone(result);
+
+        if (this._suiteLevel === 0)
+            this._print('\n');
+    }
+
+    specDone(result) {
+        super.specDone(result);
+
+        const colors = {
+            passed: GREEN,
+            pending: YELLOW,
+            failed: RED,
+            disabled: undefined,
+        };
+        const symbols = {
+            passed: '✓',
+            pending: '-',
+            failed: `${this._failureCount})`,
+            disabled: 'x',
+        };
+        this._print(indenter.indent(this._color(symbols[result.status],
+            colors[result.status]), this._suiteLevel * 2 + 2));
+        this._print(` ${result.description}`);
+        if (result.time > 75)
+            this._print(` ${this._color(`(${result.time} ms)`, RED)}`);
+        else if (result.time > 40)
+            this._print(` ${this._color(`(${result.time} ms)`, YELLOW)}`);
+        if (result.pendingReason)
+            this._print(` ${this._color(`(${result.pendingReason})`, YELLOW)}`);
+        this._print('\n');
+    }
+
+    _printSpecFailureDetails(result, index) {
+        this._print(this._color(`${index + 1}) ${result.fullName}\n\n`, RED));
+
+        result.failedExpectations.forEach(failedExpectation => {
+            this._print(indenter.indent(this._color(failedExpectation.message, GRAY), 2));
+            this._print('\n');
+            this._print(indenter.indent(this.filterStack(failedExpectation.stack), 4));
+            this._print('\n\n');
+        });
+    }
+
+    _printSuiteFailureDetails(result) {
+        result.failedExpectations.forEach(failedExpectation => {
+            this._print(this._color(`An error was thrown in an afterAll
+AfterAll ${failedExpectation.message}`, RED));
+        });
+    }
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/src/xmlWriter.js
@@ -0,0 +1,38 @@
+import GLib from 'gi://GLib';
+
+import {indenter} from './utils.js';
+
+export const Node = class Node {
+    constructor(name) {
+        this.name = name;
+        this.attrs = {};
+        this.children = [];
+        this.text = '';
+    }
+
+    toString() {
+        return `<?xml version="1.0" encoding="UTF-8"?>\n${_prettyprint(this)}`;
+    }
+};
+
+function _attrsToString(attrs) {
+    return Object.keys(attrs).map(key => {
+        const value = attrs[key].toString();
+        return ` ${key}="${GLib.markup_escape_text(value, -1)}"`;
+    }).join('');
+}
+
+function _prettyprint(node) {
+    if (node.children.length === 0 && node.text.length === 0)
+        return `<${node.name}${_attrsToString(node.attrs)}/>\n`;
+
+    const elementTop = `<${node.name}${_attrsToString(node.attrs)}>\n`;
+    const elementBottom = `</${node.name}>\n`;
+    const children = node.children.map(_prettyprint).join('');
+    let text = GLib.markup_escape_text(node.text, -1).trim();
+    if (text.length !== 0)
+        text += '\n';
+
+    return elementTop + indenter.indent(children, 2) + indenter.indent(text, 2) +
+        elementBottom;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/.eslintrc.yml
@@ -0,0 +1,29 @@
+---
+env:
+  jasmine: true
+rules:
+  no-restricted-globals:
+    - error
+    - name: fdescribe
+      message: Do not commit fdescribe(). Use describe() instead.
+    - name: fit
+      message: Do not commit fit(). Use it() instead.
+  no-restricted-syntax:
+    - error
+    - selector: CallExpression[callee.name="it"] > ArrowFunctionExpression
+      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
+    - selector: CallExpression[callee.name="describe"] > ArrowFunctionExpression
+      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
+    - selector: CallExpression[callee.name="beforeEach"] > ArrowFunctionExpression
+      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
+    - selector: CallExpression[callee.name="afterEach"] > ArrowFunctionExpression
+      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
+    - selector: CallExpression[callee.name="beforeAll"] > ArrowFunctionExpression
+      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
+    - selector: CallExpression[callee.name="afterAll"] > ArrowFunctionExpression
+      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
+globals:
+  clearInterval: writable
+  clearTimeout: writable
+  setInterval: writable
+  setTimeout: writable
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/0_your_first_suite.js
@@ -0,0 +1,705 @@
+// This suite is Jasmine's documentation suite, exercising all of Jasmine's
+// functionality to make sure that the GJS test runner has hooked everything up
+// correctly. It's taken from the following pages:
+//    https://github.com/jasmine/jasmine.github.io/blob/3.99-4.0/_tutorials/src/0_your_first_suite.js
+//
+// Note: the "long asynchronous specs" suite near the bottom takes 9 seconds to
+// run. It is marked pending by default. To run this suite anyway, define an
+// environment variable RUN_THOROUGH_TESTS=yes.
+
+import GLib from 'gi://GLib';
+
+/**
+ ## Suites: `describe` Your Tests
+ The [describe](/api/edge/global.html#describe) function is for grouping related specs, typically each test file has one at the top level.
+ The string parameter is for naming the collection of specs, and will be concatenated with specs to make a spec's full name.
+ This aids in finding specs in a large suite.
+ If you name them well, your specs read as full sentences in traditional [BDD](http://en.wikipedia.org/wiki/Behavior-driven_development) style.
+
+ ## Specs
+ Specs are defined by calling the global Jasmine function [it](/api/edge/global.html#it), which, like `describe` takes a string and a function.
+ The string is the title of the spec and the function is the spec, or test.
+ A spec contains one or more expectations that test the state of the code.
+ An expectation in Jasmine is an assertion that is either true or false.
+ A spec with all true expectations is a passing spec. A spec with one or more false expectations is a failing spec.
+ */
+describe("A suite", function() {
+  it("contains spec with an expectation", function() {
+    expect(true).toBe(true);
+  });
+});
+
+/**
+ ### It's Just Functions
+ Since `describe` and `it` blocks are functions, they can contain any executable code necessary to implement the test. JavaScript scoping rules apply, so variables declared in a `describe` are available to any `it` block inside the suite.
+ */
+describe("A suite is just a function", function() {
+  var a;
+
+  it("and so is a spec", function() {
+    a = true;
+
+    expect(a).toBe(true);
+  });
+});
+
+/**
+ ## Expectations
+ Expectations are built with the function `expect` which takes a value, called the actual.
+ It is chained with a Matcher function, which takes the expected value.
+ */
+describe("The 'toBe' matcher compares with ===", function() {
+  /**
+   ### Matchers
+   Each matcher implements a boolean comparison between the actual value and the expected value.
+   It is responsible for reporting to Jasmine if the expectation is true or false.
+   Jasmine will then pass or fail the spec.
+   */
+
+  it("and has a positive case", function() {
+    expect(true).toBe(true);
+  });
+
+  /**
+   Any matcher can evaluate to a negative assertion by chaining the call to `expect` with a `not` before calling the matcher.
+   */
+
+  it("and can have a negative case", function() {
+    expect(false).not.toBe(true);
+  });
+
+/**
+ Jasmine has a rich set of matchers included, you can find the full list in the [API docs](/api/edge/matchers.html)
+ There is also the ability to write [custom matchers](custom_matcher.html) for when a project's domain calls for specific assertions that are not included in Jasmine.
+ */
+});
+
+/**
+ ### Setup and Teardown
+ To help a test suite DRY up any duplicated setup and teardown code, Jasmine provides the global [beforeEach](/api/edge/global.html#beforeEach), [afterEach](/api/edge/global.html#afterEach), [beforeAll](/api/edge/global.html#beforeAll), and [afterAll](/api/edge/global.html#afterAll) functions.
+ */
+describe("A suite with some shared setup", function() {
+  var foo = 0;
+
+  // As the name implies, the `beforeEach` function is called once before each spec in the `describe` in which it is called
+  beforeEach(function() {
+    foo += 1;
+  });
+
+  // and the `afterEach` function is called once after each spec.
+  afterEach(function() {
+    foo = 0;
+  });
+
+  // The `beforeAll` function is called only once before all the specs in `describe` are run
+  beforeAll(function() {
+    foo = 1;
+  });
+
+  // and the `afterAll` function is called after all specs finish
+  afterAll(function() {
+    foo = 0;
+  });
+
+});
+
+/**
+ * `beforeAll` and `afterAll` can be used to speed up test suites with expensive setup and teardown.
+ *
+ * However, be careful using `beforeAll` and `afterAll`!
+ * Since they are not reset between specs, it is easy to accidentally leak state between your specs so that they erroneously pass or fail.
+ */
+
+
+/**
+### The `this` keyword
+Another way to share variables between a `beforeEach`, `it`, and `afterEach` is through the `this` keyword. Each spec's `beforeEach`/`it`/`afterEach` has the `this` as the same empty object that is set back to empty for the next spec's `beforeEach`/`it`/`afterEach`.
+*/
+describe("A spec", function() {
+  beforeEach(function() {
+    this.foo = 0;
+  });
+
+  it("can use the `this` to share state", function() {
+    expect(this.foo).toEqual(0);
+    this.bar = "test pollution?";
+  });
+
+  it("prevents test pollution by having an empty `this` created for the next spec", function() {
+    expect(this.foo).toEqual(0);
+    expect(this.bar).toBe(undefined);
+  });
+});
+
+
+/**
+ ### Manually failing a spec with `fail`
+ The `fail` function causes a spec to fail. It can take a failure message or an Error object as a parameter.
+ */
+
+describe("A spec using the fail function", function() {
+  var foo = function(x, callBack) {
+    if (x) {
+      callBack();
+    }
+  };
+
+  it("should not call the callBack", function() {
+    foo(false, function() {
+      fail("Callback has been called");
+    });
+  });
+});
+
+/**
+ ### Nesting `describe` Blocks
+ Calls to `describe` can be nested, with specs defined at any level. This allows a suite to be composed as a tree of functions. Before a spec is executed, Jasmine walks down the tree executing each `beforeEach` function in order. After the spec is executed, Jasmine walks through the `afterEach` functions similarly.
+ */
+describe("A spec", function() {
+  var foo;
+
+  beforeEach(function() {
+    foo = 0;
+    foo += 1;
+  });
+
+  afterEach(function() {
+    foo = 0;
+  });
+
+  it("is just a function, so it can contain any code", function() {
+    expect(foo).toEqual(1);
+  });
+
+  it("can have more than one expectation", function() {
+    expect(foo).toEqual(1);
+    expect(true).toEqual(true);
+  });
+
+  describe("nested inside a second describe", function() {
+    var bar;
+
+    beforeEach(function() {
+      bar = 1;
+    });
+
+    it("can reference both scopes as needed", function() {
+      expect(foo).toEqual(bar);
+    });
+  });
+});
+
+/**
+ ## Disabling Suites
+ Suites can be disabled with the `xdescribe` function. These suites and any specs inside them are skipped when run and thus their results will show as pending.
+ */
+xdescribe("A spec", function() {
+  var foo;
+
+  beforeEach(function() {
+    foo = 0;
+    foo += 1;
+  });
+
+  it("is just a function, so it can contain any code", function() {
+    expect(foo).toEqual(1);
+  });
+});
+
+/**
+ ## Pending Specs
+ Pending specs do not run, but their names will show up in the results as `pending`.
+ */
+
+describe("Pending specs", function() {
+
+  /** Any spec declared with `xit` is marked as pending.
+   */
+  xit("can be declared 'xit'", function() {
+    expect(true).toBe(false);
+  });
+
+  /** Any spec declared without a function body will also be marked pending in results.
+   */
+
+  it("can be declared with 'it' but without a function");
+
+  /** And if you call the function `pending` anywhere in the spec body, no matter the expectations, the spec will be marked pending.
+   * A string passed to `pending` will be treated as a reason and displayed when the suite finishes.
+   */
+  it("can be declared by calling 'pending' in the spec body", function() {
+    expect(true).toBe(false);
+    pending('this is why it is pending');
+  });
+});
+
+/**
+ ## Spies
+ Jasmine has test double functions called [spies](/api/edge/Spy.html).
+ A spy can stub any function and tracks calls to it and all arguments.
+ A spy only exists in the `describe` or `it` block in which it is defined, and will be removed after each spec.
+ There are special matchers for interacting with spies.
+ */
+
+describe("A spy", function() {
+  var foo, bar = null;
+
+  beforeEach(function() {
+    foo = {
+      setBar: function(value) {
+        bar = value;
+      }
+    };
+
+    // You can define what the spy will do when invoked with [and](/api/edge/Spy_and.html).
+    spyOn(foo, 'setBar');
+
+    foo.setBar(123);
+    foo.setBar(456, 'another param');
+  });
+
+  // The `toHaveBeenCalled` matcher will pass if the spy was called.
+  it("tracks that the spy was called", function() {
+    expect(foo.setBar).toHaveBeenCalled();
+  });
+
+  // The `toHaveBeenCalledTimes` matcher will pass if the spy was called the specified number of times.
+  it("tracks that the spy was called x times", function() {
+    expect(foo.setBar).toHaveBeenCalledTimes(2);
+  });
+
+  // The `toHaveBeenCalledWith` matcher will return true if the argument list matches any of the recorded calls to the spy.
+  it("tracks all the arguments of its calls", function() {
+    expect(foo.setBar).toHaveBeenCalledWith(123);
+    expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
+  });
+
+  it("stops all execution on a function", function() {
+    expect(bar).toBeNull();
+  });
+
+  // You get all of the data that a spy tracks about its calls with [calls](/api/edge/Spy_calls.html)
+  it("tracks if it was called at all", function() {
+    foo.setBar();
+
+    expect(foo.setBar.calls.any()).toEqual(true);
+  });
+});
+
+/**
+ ### Spies: `createSpy`
+ When there is not a function to spy on, [jasmine.createSpy](/api/edge/jasmine.html#.createSpy) can create a "bare" spy.
+ This spy acts as any other spy - tracking calls, arguments, etc. But there is no implementation behind it.
+ */
+describe("A spy, when created manually", function() {
+  var whatAmI;
+
+  beforeEach(function() {
+    whatAmI = jasmine.createSpy('whatAmI');
+
+    whatAmI("I", "am", "a", "spy");
+  });
+
+  it("tracks that the spy was called", function() {
+    expect(whatAmI).toHaveBeenCalled();
+  });
+});
+
+/**
+ ### Spies: `createSpyObj`
+ In order to create a mock with multiple spies, use [jasmine.createSpyObj](/api/edge/jasmine.html#.createSpyObj) and pass an array of strings.
+ It returns an object that has a property for each string that is a spy.
+ */
+describe("Multiple spies, when created manually", function() {
+  var tape;
+
+  beforeEach(function() {
+    tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
+
+    tape.play();
+    tape.pause();
+    tape.rewind(0);
+  });
+
+  it("creates spies for each requested function", function() {
+    expect(tape.play).toBeDefined();
+    expect(tape.pause).toBeDefined();
+    expect(tape.stop).toBeDefined();
+    expect(tape.rewind).toBeDefined();
+  });
+});
+
+/**
+ * ## Matching with more finesse
+ *
+ * Sometimes you don't want to match with exact equality.
+ * Jasmine provides a number of asymmetric equality testers.
+ */
+describe("Matching with finesse", function() {
+
+  /**
+   * [jasmine.any](/api/edge/jasmine.html#.any) takes a constructor or "class" name as an expected value.
+   * It returns `true` if the constructor matches the constructor of the actual value.
+   */
+  describe("jasmine.any", function() {
+    it("matches any value", function() {
+      expect({}).toEqual(jasmine.any(Object));
+      expect(12).toEqual(jasmine.any(Number));
+    });
+
+    describe("when used with a spy", function() {
+      it("is useful for comparing arguments", function() {
+        var foo = jasmine.createSpy('foo');
+        foo(12, function() {
+          return true;
+        });
+
+        expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
+      });
+    });
+  });
+
+  /**
+   [jasmine.anything](/api/edge/global.html#.anything) returns `true` if the actual value is not `null` or `undefined`.
+   */
+  describe("jasmine.anything", function() {
+    it("matches anything", function() {
+      expect(1).toEqual(jasmine.anything());
+    });
+
+    describe("when used with a spy", function() {
+      it("is useful when the argument can be ignored", function() {
+        var foo = jasmine.createSpy('foo');
+        foo(12, function() {
+          return false;
+        });
+
+        expect(foo).toHaveBeenCalledWith(12, jasmine.anything());
+      });
+    });
+  });
+
+  /**
+   [jasmine.objectContaining](/api/edge/global.html#.objectContaining) is for those times when an expectation only cares about certain key/value pairs in the actual.
+   */
+  describe("jasmine.objectContaining", function() {
+    var foo;
+
+    beforeEach(function() {
+      foo = {
+        a: 1,
+        b: 2,
+        bar: "baz"
+      };
+    });
+
+    it("matches objects with the expect key/value pairs", function() {
+      expect(foo).toEqual(jasmine.objectContaining({
+        bar: "baz"
+      }));
+      expect(foo).not.toEqual(jasmine.objectContaining({
+        c: 37
+      }));
+    });
+
+    describe("when used with a spy", function() {
+      it("is useful for comparing arguments", function() {
+        var callback = jasmine.createSpy('callback');
+
+        callback({
+          bar: "baz"
+        });
+
+        expect(callback).toHaveBeenCalledWith(jasmine.objectContaining({
+          bar: "baz"
+        }));
+      });
+    });
+  });
+
+  /**
+   [jasmine.arrayContaining](/api/edge/global.html#.arrayContaining) is for those times when an expectation only cares about some of the values in an array.
+   */
+  describe("jasmine.arrayContaining", function() {
+    var foo;
+
+    beforeEach(function() {
+      foo = [1, 2, 3, 4];
+    });
+
+    it("matches arrays with some of the values", function() {
+      expect(foo).toEqual(jasmine.arrayContaining([3, 1]));
+      expect(foo).not.toEqual(jasmine.arrayContaining([6]));
+    });
+
+    describe("when used with a spy", function() {
+      it("is useful when comparing arguments", function() {
+        var callback = jasmine.createSpy('callback');
+
+        callback([1, 2, 3, 4]);
+
+        expect(callback).toHaveBeenCalledWith(jasmine.arrayContaining([4, 2, 3]));
+        expect(callback).not.toHaveBeenCalledWith(jasmine.arrayContaining([5, 2]));
+      });
+    });
+  });
+
+  /**
+   [jasmine.stringMatching](/api/edge/global.html#.stringMatching) is for when you don't want to match a string in a larger object exactly, or match a portion of a string in a spy expectation.
+   */
+  describe('jasmine.stringMatching', function() {
+    it("matches as a regexp", function() {
+      expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)});
+      expect({foo: 'foobarbaz'}).toEqual({foo: jasmine.stringMatching('bar')});
+    });
+
+    describe("when used with a spy", function() {
+      it("is useful for comparing arguments", function() {
+        var callback = jasmine.createSpy('callback');
+
+        callback('foobarbaz');
+
+        expect(callback).toHaveBeenCalledWith(jasmine.stringMatching('bar'));
+        expect(callback).not.toHaveBeenCalledWith(jasmine.stringMatching(/^bar$/));
+      });
+    });
+  });
+
+  /**
+   ### Custom asymmetric equality tester
+   When you need to check that something meets a certain criteria, without being strictly equal, you can also specify a custom asymmetric equality tester simply by providing an object that has an `asymmetricMatch` function.
+   */
+  describe("custom asymmetry", function() {
+    var tester = {
+      asymmetricMatch: function(actual) {
+        var secondValue = actual.split(',')[1];
+        return secondValue === 'bar';
+      }
+    };
+
+    it("dives in deep", function() {
+      expect("foo,bar,baz,quux").toEqual(tester);
+    });
+
+    describe("when used with a spy", function() {
+      it("is useful for comparing arguments", function() {
+        var callback = jasmine.createSpy('callback');
+
+        callback('foo,bar,baz');
+
+        expect(callback).toHaveBeenCalledWith(tester);
+      });
+    });
+  });
+});
+
+
+/**
+ ## Jasmine Clock
+ The [Jasmine Clock](/api/edge/Clock.html) is available for testing time dependent code.
+ */
+describe("Manually ticking the Jasmine Clock", function() {
+  var timerCallback;
+
+  /**
+   It is installed with a call to `jasmine.clock().install` in a spec or suite that needs to manipulate time.
+   */
+  beforeEach(function() {
+    timerCallback = jasmine.createSpy("timerCallback");
+    jasmine.clock().install();
+  });
+
+  /**
+   Be sure to uninstall the clock after you are done to restore the original functions.
+   */
+  afterEach(function() {
+    jasmine.clock().uninstall();
+  });
+
+  /**
+   ### Mocking the JavaScript Timeout Functions
+   You can make `setTimeout` or `setInterval` synchronous executing the registered functions only once the clock is ticked forward in time.
+
+   To execute registered functions, move time forward via the `jasmine.clock().tick` function, which takes a number of milliseconds.
+   */
+  it("causes a timeout to be called synchronously", function() {
+    setTimeout(function() {
+      timerCallback();
+    }, 100);
+
+    expect(timerCallback).not.toHaveBeenCalled();
+
+    jasmine.clock().tick(101);
+
+    expect(timerCallback).toHaveBeenCalled();
+  });
+
+  it("causes an interval to be called synchronously", function() {
+    setInterval(function() {
+      timerCallback();
+    }, 100);
+
+    expect(timerCallback).not.toHaveBeenCalled();
+
+    jasmine.clock().tick(101);
+    expect(timerCallback.calls.count()).toEqual(1);
+
+    jasmine.clock().tick(50);
+    expect(timerCallback.calls.count()).toEqual(1);
+
+    jasmine.clock().tick(50);
+    expect(timerCallback.calls.count()).toEqual(2);
+  });
+
+  /**
+   ### Mocking the Date
+   The Jasmine Clock can also be used to mock the current date.
+  */
+  describe("Mocking the Date object", function(){
+    it("mocks the Date object and sets it to a given time", function() {
+      var baseTime = new Date(2013, 9, 23);
+      // If you do not provide a base time to `mockDate` it will use the current date.
+      jasmine.clock().mockDate(baseTime);
+
+      jasmine.clock().tick(50);
+      expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
+    });
+  });
+});
+
+
+/**
+ ## Asynchronous Support
+ Jasmine also has support for running specs that require testing asynchronous operations. The functions that you pass to `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, and `it` can be asynchronous. There are three different ways to indicate that a function is asynchronous: by taking an optional callback parameter, by returning a promise, or by using the `async` keyword in environments that support it.
+ */
+describe("Asynchronous specs", function() {
+  var value;
+
+  /**
+   ### Using callbacks
+   */
+  describe("Using callbacks", function() {
+    /**
+     Calls to `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, and `it` can take an optional single argument that should be called when the async work is complete.
+     */
+    beforeEach(function(done) {
+      setTimeout(function() {
+        value = 0;
+        done();
+      }, 1);
+    });
+
+    /**
+     This spec will not start until the `done` function is called in the call to `beforeEach` above. And this spec will not complete until its `done` is called.
+     */
+
+    it("should support async execution of test preparation and expectations", function(done) {
+      value++;
+      expect(value).toBeGreaterThan(0);
+      done();
+    });
+
+    /**
+     The `done.fail` function fails the spec and indicates that it has completed.
+     */
+    describe("A spec using done.fail", function() {
+      var foo = function(x, callBack1, callBack2) {
+        if (x) {
+          setTimeout(callBack1, 0);
+        } else {
+          setTimeout(callBack2, 0);
+        }
+      };
+
+      it("should not call the second callBack", function(done) {
+        foo(true,
+          done,
+          function() {
+            done.fail("Second callback has been called");
+          }
+        );
+      });
+    });
+  });
+
+  /**
+   ### Using promises
+   */
+  describe("Using promises", function() {
+    /**
+     Functions passed to `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, and `it` can return a promise that should be resolved when the async work is complete. If the promise is rejected, all specs in the enclosing `describe` will fail.
+     */
+    beforeEach(function() {
+      return soon().then(function() {
+        value = 0;
+      });
+    });
+
+    /**
+     This spec will not start until the promise returned from the call to `beforeEach` above is settled. And this spec will not complete until the promise that it returns is settled. If the promise is rejected, the spec will fail.
+     */
+    it("should support async execution of test preparation and expectations", function() {
+      return soon().then(function() {
+        value++;
+        expect(value).toBeGreaterThan(0);
+      });
+    });
+  });
+
+  /**
+   ### Using async/await
+   */
+  describe("Using async/await", function() {
+    /**
+     Functions passed to `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, and `it` can be declared `async` in environments that support `async`/`await`.
+     */
+    beforeEach(async function() {
+      await soon();
+      value = 0;
+    });
+
+    /**
+     This spec will not start until the promise returned from the call to `beforeEach` above is settled. And this spec will not complete until the promise that it returns is settled.
+     */
+    it("should support async execution of test preparation and expectations", async function() {
+      await soon();
+      value++;
+      expect(value).toBeGreaterThan(0);
+    });
+  });
+
+
+  /**
+   By default jasmine will wait for 5 seconds for an asynchronous spec to finish before causing a timeout failure.
+   If the timeout expires before `done` is called, the current spec will be marked as failed and suite execution will continue as if `done` was called.
+
+   If specific specs should fail faster or need more time this can be adjusted by passing a timeout value to `it`, etc.
+
+   If the entire suite should have a different timeout, `jasmine.DEFAULT_TIMEOUT_INTERVAL` can be set globally, outside of any given `describe`.
+   */
+  describe("long asynchronous specs", function() {
+    beforeEach(function(done) {
+      done();
+    }, 1000);
+
+    if (GLib.getenv('RUN_THOROUGH_TESTS') === 'yes') {
+      it("takes a long time", function(done) {
+        setTimeout(function() {
+          done();
+        }, 9000);
+      }, 10000);
+    }
+
+    afterEach(function(done) {
+      done();
+    }, 1000);
+  });
+
+  function soon() {
+    return new Promise(function(resolve, reject) {
+      setTimeout(function() {
+        resolve();
+      }, 1);
+    });
+  }
+});
\ No newline at end of file
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/commandSpec.js
@@ -0,0 +1,125 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import * as Command from '../src/command.js';
+import * as JUnitReporter from '../src/junitReporter.js';
+import * as TapReporter from '../src/tapReporter.js';
+import * as VerboseReporter from '../src/verboseReporter.js';
+
+const {mainloop} = Command;
+
+describe('Jasmine command', function () {
+    let fakeJasmine;
+
+    beforeAll(function () {
+        Gio._promisify(Gio._LocalFilePrototype, 'delete_async', 'delete_finish');
+    });
+
+    beforeEach(function () {
+        fakeJasmine = jasmine.createSpyObj('jasmine', ['addReporter', 'configureDefaultReporter', 'execute']);
+        fakeJasmine.execute.and.resolveTo();
+        spyOn(mainloop, 'runAsync');  // stub out system behaviour
+    });
+
+    describe('running specs', function () {
+        it('shows colors by default', function () {
+            Command.run(fakeJasmine, []);
+            expect(fakeJasmine.addReporter)
+                .toHaveBeenCalledWith(jasmine.objectContaining({show_colors: true}));
+        });
+
+        it('allows colors to be turned off', function () {
+            Command.run(fakeJasmine, ['--no-color']);
+            expect(fakeJasmine.addReporter)
+                .toHaveBeenCalledWith(jasmine.objectContaining({show_colors: false}));
+        });
+
+        it('lets later color arguments override earlier ones', function () {
+            Command.run(fakeJasmine, ['--color', '--no-color']);
+            expect(fakeJasmine.addReporter)
+                .toHaveBeenCalledWith(jasmine.objectContaining({show_colors: false}));
+            Command.run(fakeJasmine, ['--no-color', '--color']);
+            expect(fakeJasmine.addReporter)
+                .toHaveBeenCalledWith(jasmine.objectContaining({show_colors: true}));
+        });
+
+        it('loads the verbose reporter', function () {
+            Command.run(fakeJasmine, ['--verbose']);
+            expect(fakeJasmine.addReporter).toHaveBeenCalled();
+            const [reporter] = fakeJasmine.addReporter.calls.argsFor(0);
+            expect(reporter.constructor).toBe(VerboseReporter.VerboseReporter);
+        });
+
+        it('loads the TAP reporter', function () {
+            Command.run(fakeJasmine, ['--tap']);
+            expect(fakeJasmine.addReporter).toHaveBeenCalled();
+            const [reporter] = fakeJasmine.addReporter.calls.argsFor(0);
+            expect(reporter.constructor).toBe(TapReporter.TapReporter);
+        });
+
+        it('loads the JUnit reporter alongside the usual reporter', function () {
+            // Unfortunately /dev/null can't be opened as a GFile, so we need to
+            // write to a temporary file.
+            const [tmpFile, stream] = Gio.File.new_tmp('junitreportXXXXXX');
+            const tmpPath = tmpFile.get_path();
+            stream.close(null);
+
+            Command.run(fakeJasmine, ['--junit', tmpPath]);
+            expect(fakeJasmine.addReporter.calls.count()).toBe(2);
+            const reporters = fakeJasmine.addReporter.calls.allArgs().map(args => args[0].constructor);
+            expect(reporters).toContain(JUnitReporter.JUnitReporter);
+
+            tmpFile.delete_async(GLib.PRIORITY_DEFAULT, null).then(() => {});
+        });
+
+        it('creates a directory for the report if necessary', function () {
+            const tmpDir = GLib.dir_make_tmp('junitreportXXXXXX');
+            const tmpFile =
+                Gio.File.new_for_path(tmpDir).get_child('dir').get_child('report.xml');
+            const tmpPath = tmpFile.get_path();
+
+            Command.run(fakeJasmine, ['--junit', tmpPath]);
+            expect(tmpFile.query_exists(null)).toBeTruthy();
+
+            tmpFile.delete_async(GLib.PRIORITY_DEFAULT, null)
+                .then(() => tmpFile.get_parent().delete_async(GLib.PRIORITY_DEFAULT, null))
+                .then(() => tmpFile.get_parent().get_parent().delete_async(GLib.PRIORITY_DEFAULT, null));
+        });
+
+        it('uses the value of JASMINE_JUNIT_REPORTS_DIR', function () {
+            const oldPath = GLib.getenv('JASMINE_JUNIT_REPORTS_DIR');
+            const tmpDir = GLib.dir_make_tmp('junitreportXXXXXX');
+            GLib.setenv('JASMINE_JUNIT_REPORTS_DIR', tmpDir, true);
+
+            Command.run(fakeJasmine, ['--junit', 'report.xml']);
+            const reportFile =
+                Gio.File.new_for_path(tmpDir).get_child('report.xml');
+            expect(reportFile.query_exists(null)).toBeTruthy();
+
+            reportFile.delete_async(GLib.PRIORITY_DEFAULT, null)
+                .then(() => reportFile.get_parent().delete_async(GLib.PRIORITY_DEFAULT, null));
+
+            if (oldPath !== null)
+                GLib.setenv('JASMINE_JUNIT_REPORTS_DIR', oldPath, true);
+            else
+                GLib.unsetenv('JASMINE_JUNIT_REPORTS_DIR');
+        });
+
+        it('executes the Jasmine suite', function (done) {
+            expectAsync(Command.run(fakeJasmine, [])).toBeResolvedTo(0);
+            // fakeJasmine.execute() is started in idle
+            GLib.idle_add(GLib.PRIORITY_DEFAULT, function () {
+                expect(fakeJasmine.execute).toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('runs the specified specs', function (done) {
+            Command.run(fakeJasmine, ['spec/some/fileSpec.js', '--no-color']);
+            GLib.idle_add(GLib.PRIORITY_DEFAULT, function () {
+                expect(fakeJasmine.execute).toHaveBeenCalledWith(['spec/some/fileSpec.js']);
+                done();
+            });
+        });
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/configSpec.js
@@ -0,0 +1,270 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import * as Config from '../src/config.js';
+import * as Options from '../src/options.js';
+
+const [testFile] = GLib.filename_from_uri(import.meta.url);
+const testDir = GLib.path_get_dirname(testFile);
+
+describe('Ensure array', function () {
+    it('does not change an array', function () {
+        expect(Config.ensureArray(['a', 'b'])).toEqual(['a', 'b']);
+    });
+
+    it('puts a single value into an array', function () {
+        expect(Config.ensureArray('a')).toEqual(['a']);
+    });
+});
+
+describe('Loading configuration', function () {
+    beforeEach(function () {
+        // suppress messages
+        spyOn(globalThis, 'print');
+        spyOn(globalThis, 'printerr');
+    });
+
+    it('loads from a file', function () {
+        const config = Config.loadConfig({config: `${testDir}/fixtures/jasmine.json`});
+        expect(config.a).toEqual('b');
+        expect(config.c).toEqual('d');
+    });
+
+    it("doesn't load the file if no-config specified", function () {
+        const config = Config.loadConfig({
+            'no-config': true,
+            config: `${testDir}/fixtures/jasmine.json`,
+        });
+        const keys = Object.keys(config);
+        expect(keys).not.toContain('a');
+        expect(keys).not.toContain('c');
+    });
+
+    it('loads the default file if none given', function () {
+        const config = Config.loadConfig({}, `${testDir}/fixtures/jasmine.json`);
+        expect(config.a).toEqual('b');
+        expect(config.c).toEqual('d');
+    });
+
+    it("errors out if the file doesn't exist", function () {
+        expect(() => Config.loadConfig({config: 'nonexist.json'})).toThrow();
+    });
+
+    it("doesn't error out if the default file doesn't exist", function () {
+        expect(() => Config.loadConfig({}, 'nonexist.json')).not.toThrow();
+    });
+
+    it('errors out if the file is invalid', function () {
+        expect(() => Config.loadConfig({
+            config: `${testDir}/fixtures/invalid.json`,
+        })).toThrow();
+    });
+
+    it("resolves paths relative to the config file's location", function () {
+        const config = Config.loadConfig({config: `${testDir}/fixtures/path.json`});
+        const location = Gio.File.new_for_path(`${testDir}/fixtures`);
+
+        expect(config.include_paths).toContain(location.get_path());
+        expect(config.spec_files).toContain(location.get_child('someSpec.js').get_path());
+    });
+
+    it('warns about unrecognized config options', function () {
+        Config.loadConfig({config: `${testDir}/fixtures/jasmine.json`});
+        expect(globalThis.printerr).toHaveBeenCalledWith(jasmine.stringMatching(/^warning: /));
+    });
+});
+
+describe('Configuration options to arguments', function () {
+    it('lets command line arguments override config options', function () {
+        const args = Config.configToArgs({options: '--color'},
+            ...Options.parseOptions(['--no-color']));
+        expect(args.indexOf('--no-color')).toBeGreaterThan(args.indexOf('--color'));
+    });
+
+    it('adds one exclusion path', function () {
+        const args = Config.configToArgs({exclude: 'a'});
+        expect(args.join(' ')).toMatch('--exclude a');
+    });
+
+    it('adds more than one exclusion path', function () {
+        const args = Config.configToArgs({exclude: ['a', 'b']});
+        expect(args.join(' ')).toMatch('--exclude a');
+        expect(args.join(' ')).toMatch('--exclude b');
+    });
+
+    it('adds exclusions from the command line', function () {
+        const args = Config.configToArgs({}, ...Options.parseOptions(['--exclude', 'a.js']));
+        expect(args.join(' ')).toMatch('--exclude a.js');
+    });
+
+    it('combines exclusions from the command line and the config file', function () {
+        const args = Config.configToArgs({exclude: 'b.js'},
+            ...Options.parseOptions(['--exclude', 'a.js']));
+        expect(args.join(' ')).toMatch('--exclude a.js');
+        expect(args.join(' ')).toMatch('--exclude b.js');
+    });
+
+    it('adds one extra option', function () {
+        const args = Config.configToArgs({options: '--foo'});
+        expect(args).toContain('--foo');
+    });
+
+    it('adds more than one extra option', function () {
+        const args = Config.configToArgs({options: ['--foo', '--bar']});
+        expect(args.join(' ')).toMatch('--foo --bar');
+        // order should be preserved here
+    });
+
+    it('adds one spec file', function () {
+        const args = Config.configToArgs({spec_files: 'a'});
+        expect(args).toContain('a');
+    });
+
+    it('adds more than one spec file', function () {
+        const args = Config.configToArgs({spec_files: ['a', 'b']});
+        expect(args).toContain('a');
+        expect(args).toContain('b');
+    });
+
+    it('does not add spec files from config if there were some on the command line', function () {
+        const args = Config.configToArgs({spec_files: ['a', 'b']}, ['c']);
+        expect(args).not.toContain('a');
+        expect(args).not.toContain('b');
+    });
+
+    it('passes the arguments on to the subprocess', function () {
+        const args = Config.configToArgs({}, ...Options.parseOptions(['--color', 'spec.js']));
+        expect(args).toContain('--color', 'spec.js');
+    });
+
+    it('passes the config file on to the subprocess as arguments', function () {
+        const args = Config.configToArgs({
+            environment: {},
+            options: ['--color'],
+            exclude: ['nonspec*.js'],
+            spec_files: ['a.js', 'b.js'],
+        }, [], {});
+        expect(args.join(' ')).toMatch(/--exclude nonspec\*\.js/);
+        expect(args).toContain('--color', 'a.js', 'b.js');
+    });
+
+    it('does not pass the config file specs if specs were on the command line', function () {
+        const args = Config.configToArgs({
+            environment: {},
+            spec_files: ['spec2.js'],
+        }, ['spec1.js']);
+        expect(args).toContain('spec1.js');
+        expect(args).not.toContain('spec2.js');
+    });
+
+    it('does not pass include paths as -I arguments', function () {
+        const args = Config.configToArgs({
+            environment: {},
+            include_paths: ['/path1', '/path2'],
+        }, [], {});
+        expect(args.join(' ')).not.toMatch(/-I/);
+    });
+});
+
+describe('Manipulating the environment', function () {
+    it('sets environment variables in the launcher', function () {
+        const launcher = Config.prepareLauncher({
+            environment: {
+                'MY_VARIABLE': 'my_value',
+            },
+        });
+        expect(launcher.getenv('MY_VARIABLE')).toEqual('my_value');
+    });
+
+    it('unsets environment variables with null values', function () {
+        const launcher = Config.prepareLauncher({
+            environment: {
+                'MY_VARIABLE': null,
+            },
+        });
+        expect(launcher.getenv('MY_VARIABLE')).toBeNull();
+    });
+
+    it('adds one search path', function () {
+        const launcher = Config.prepareLauncher({include_paths: '/a'});
+        expect(launcher.getenv('GJS_PATH')).toEqual('/a');
+    });
+
+    it('adds multiple search paths in the right order', function () {
+        const launcher = Config.prepareLauncher({include_paths: ['/a', '/b']});
+        expect(launcher.getenv('GJS_PATH')).toEqual('/a:/b');
+    });
+
+    it('adds search paths with a lower priority than existing search paths', function () {
+        const oldPath = GLib.getenv('GJS_PATH');
+        GLib.setenv('GJS_PATH', '/a:/b', /* overwrite = */ true);
+        const launcher = Config.prepareLauncher({include_paths: ['/c', '/d']});
+        expect(launcher.getenv('GJS_PATH')).toEqual('/a:/b:/c:/d');
+        if (oldPath)
+            GLib.setenv('GJS_PATH', oldPath, /* overwrite = */ true);
+        else
+            GLib.unsetenv('GJS_PATH');
+    });
+});
+
+describe('Manipulating the launcher command line', function () {
+    let args;
+
+    beforeEach(function () {
+        args = ['jasmine-runner', '--verbose', 'foo.js'];
+    });
+
+    it('executes jasmine-runner with a different interpreter binary', function () {
+        args = Config.wrapArgs(args, {
+            interpreter: '/path/to/custom/gjs',
+        });
+        expect(args).toEqual(['/path/to/custom/gjs', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+
+    it('allows adding arguments to the interpreter', function () {
+        args = Config.wrapArgs(args, {
+            interpreter: 'gjs -d',
+        });
+        expect(args).toEqual(['gjs', '-d', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+
+    it('executes jasmine-runner with a different interpreter binary from the command line', function () {
+        args = Config.wrapArgs(args, {}, {
+            interpreter: '/path/to/custom/gjs',
+        });
+        expect(args).toEqual(['/path/to/custom/gjs', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+
+    it('gives the interpreter specified on the command line priority', function () {
+        args = Config.wrapArgs(args, {
+            interpreter: '/path/to/other/gjs',
+        }, {
+            interpreter: '/path/to/custom/gjs',
+        });
+        expect(args).toEqual(['/path/to/custom/gjs', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+
+    it('executes jasmine-runner with a debugger', function () {
+        args = Config.wrapArgs(args, {}, {
+            debug: 'gdb --args',
+        });
+        expect(args).toEqual(['gdb', '--args', 'gjs', '-m', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+
+    it('does not pass the gjs interpreter to the debugger if a custom one is configured', function () {
+        args = Config.wrapArgs(args, {
+            interpreter: '/path/to/custom/gjs',
+        }, {
+            debug: 'lldb --',
+        });
+        expect(args).toEqual(['lldb', '--', '/path/to/custom/gjs', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+
+    it('does not pass the gjs interpreter to the debugger if a custom one is given on the command line', function () {
+        args = Config.wrapArgs(args, {}, {
+            debug: 'lldb --',
+            interpreter: '/path/to/custom/gjs',
+        });
+        expect(args).toEqual(['lldb', '--', '/path/to/custom/gjs', 'jasmine-runner', '--verbose', 'foo.js']);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/consoleReporterSpec.js
@@ -0,0 +1,118 @@
+import * as ConsoleReporter from '../src/consoleReporter.js';
+
+describe('Console reporter base class', function () {
+    let reporter, timerSpies;
+    const jasmineCorePath = 'path/to/jasmine/core/jasmine.js';
+
+    beforeEach(function () {
+        timerSpies = {};
+        const timerSpy = id => {
+            timerSpies[id] = jasmine.createSpyObj('timer', ['start', 'elapsed']);
+            return timerSpies[id];
+        };
+        reporter = new ConsoleReporter.ConsoleReporter({
+            timerFactory: timerSpy,
+            jasmineCorePath,
+        });
+    });
+
+    it('can be instantiated', function () {
+        reporter = new ConsoleReporter.ConsoleReporter();
+    });
+
+    it('starts the main timer when Jasmine starts', function () {
+        reporter.jasmineStarted();
+        expect(timerSpies['main'].start).toHaveBeenCalled();
+    });
+
+    it('purges Jasmine internals from stack traces', function () {
+        const fakeStack = [
+            `foo${jasmineCorePath}`,
+            `bar ${jasmineCorePath}`,
+            'line of useful stack trace',
+            `baz ${jasmineCorePath}`,
+        ].join('\n');
+        const stackTrace = reporter.filterStack(fakeStack);
+        expect(stackTrace).toMatch('line of useful stack trace');
+        expect(stackTrace).not.toMatch(jasmineCorePath);
+    });
+
+    describe('started signal', function () {
+        beforeEach(function () {
+            reporter = new ConsoleReporter.ConsoleReporter();
+        });
+
+        it('is emitted when the suite starts', function (done) {
+            reporter.connect('started', () => done());
+            reporter.jasmineStarted();
+        });
+    });
+
+    describe('complete signal', function () {
+        beforeEach(function () {
+            reporter = new ConsoleReporter.ConsoleReporter();
+            reporter.jasmineStarted();
+        });
+
+        it('is emitted with true when the suite is done', function (done) {
+            reporter.connect('complete', (_, success) => {
+                expect(success).toBeTruthy();
+                done();
+            });
+            reporter.jasmineDone();
+        });
+
+        it('is emitted with false if there are spec failures', function (done) {
+            reporter.connect('complete', (_, success) => {
+                expect(success).toBeFalsy();
+                done();
+            });
+            reporter.specDone({status: 'failed', failedExpectations: []});
+            reporter.jasmineDone();
+        });
+
+        it('is emitted with false if there are suite failures', function (done) {
+            reporter.connect('complete', (_, success) => {
+                expect(success).toBeFalsy();
+                done();
+            });
+            reporter.specDone({status: 'passed'});
+            reporter.suiteDone({failedExpectations: [{message: 'bananas'}]});
+            reporter.jasmineDone();
+        });
+    });
+
+    it('times individual suites', function () {
+        const suiteInfo = {id: 'foo'};
+        reporter.suiteStarted(suiteInfo);
+        expect(timerSpies['suite:foo'].start).toHaveBeenCalled();
+
+        timerSpies['suite:foo'].elapsed.and.returnValue(800);
+        reporter.suiteDone(suiteInfo);
+        expect(suiteInfo.time).toBe(800);
+    });
+
+    it('times individual specs', function () {
+        const specInfo = {
+            id: 'foo',
+            status: 'passed',
+        };
+        reporter.specStarted(specInfo);
+        expect(timerSpies['spec:foo'].start).toHaveBeenCalled();
+
+        timerSpies['spec:foo'].elapsed.and.returnValue(800);
+        reporter.specDone(specInfo);
+        expect(specInfo.time).toBe(800);
+    });
+
+    it('starts a timer', function () {
+        reporter.startTimer('foobar');
+        expect(timerSpies['foobar'].start).toHaveBeenCalled();
+    });
+
+    it('gets the elapsed time from a timer', function () {
+        reporter.startTimer('foobar');
+        timerSpies['foobar'].elapsed.and.returnValue(500);
+        expect(reporter.elapsedTime('foobar')).toBe(500);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/custom_matcher.js
@@ -0,0 +1,127 @@
+/**
+ *
+ * Often a project will want to encapsulate custom matching code for use across multiple specs. Here is how to create a Jasmine-compatible custom matcher.
+ *
+ * A custom matcher at its root is a comparison function that takes an `actual` value and `expected` value. This factory is passed to Jasmine, ideally in a call to `beforeEach` and will be in scope and available for all of the specs inside a given call to `describe`. Custom matchers are torn down between specs. The name of the factory will be the name of the matcher exposed on the return value of the call to `expect`.
+ *
+ */
+
+/**
+ * This object has a custom matcher named "toBeGoofy".
+ */
+var customMatchers = {
+
+  /**
+   * ## Matcher Factories
+   *
+   * Custom matcher factories are passed a `matchersUtil` parameter, which has a set
+   * of utility functions for matchers to use to perform tasks like determining
+   * whether two objects are equal
+   * (see: [`MatchersUtil`][MatchersUtil] for reference documentation). By using
+   * `MatchersUtil` where appropriate, custom matchers can work with
+   * [custom equality testers][CustomEqualityTesters] and
+   * [custom object formatters][CustomObjectFormatters] without any extra effort.
+   *
+   * A second `customEqualityTesters` parameter is passed for compatibility with
+   * Jasmine 3.5 and earlier. Matchers written for Jasmine 3.6 and later should
+   * ignore it. It will no longer be provided in Jasmine 4.
+   *
+   * [MatchersUtil]: /api/edge/MatchersUtil.html
+   * [CustomEqualityTesters]: /tutorials/custom_equality
+   * [CustomObjectFormatters]: /tutorials/custom_object_formatters
+   */
+  toBeGoofy: function(matchersUtil) {
+    /**
+     * The factory method should return an object with a `compare` function that will be called to check the expectation.
+     */
+    return {
+      /**
+       * ## A Function to `compare`
+       *
+       * The compare function receives the value passed to `expect()` as the first argument - the actual - and the value (if any) passed to the matcher itself as second argument.
+       */
+      compare: function(actual, expected) {
+
+        /**
+         * `toBeGoofy` takes an optional `expected` argument, so define it here if not passed in.
+         */
+        if (expected === undefined) {
+          expected = '';
+        }
+
+        /**
+         * ### Result
+         *
+         * The `compare` function must return a result object with a `pass` property that is a boolean result of the matcher. The `pass` property tells the expectation whether the matcher was successful (`true`) or unsuccessful (`false`). If the expectation is called/chained with `.not`, the expectation will negate this to determine whether the expectation is met.
+         */
+        var result = {};
+
+        /**
+         * `toBeGoofy` tests for equality of the actual's `hyuk` property to see if it matches the expectation.
+         */
+        result.pass = matchersUtil.equals(actual.hyuk, "gawrsh" + expected);
+
+        /**
+         * ### Failure Messages
+         *
+         * If left `undefined`, the expectation will attempt to craft a failure message for the matcher. However, if the return value has a `message` property it will be used for a  failed expectation.
+         */
+        if (result.pass) {
+          /**
+           * The matcher succeeded, so the custom failure message should be present in the case of a negative expectation - when the expectation is used with `.not`.
+           */
+          result.message = "Expected " + actual + " not to be quite so goofy";
+        } else {
+          /**
+           * The matcher failed, so the custom failure message should be present in the case of a positive expectation
+           */
+          result.message = "Expected " + actual + " to be goofy, but it was not very goofy";
+        }
+
+        /**
+         * Return the result of the comparison.
+         */
+        return result;
+      }
+    };
+  }
+};
+
+/**
+* ### Custom negative comparators
+*
+* If you need more control over the negative comparison (the `not` case) than the simple boolean inversion above, you can also have your matcher factory include another key, `negativeCompare` alongside `compare`, for which the value is a function to invoke when `.not` is used. This function/key is optional.
+*/
+
+/**
+ * ## Registration and Usage
+ */
+describe("Custom matcher: 'toBeGoofy'", function() {
+  /**
+   * Register the custom matchers with Jasmine. All properties on the object passed in will be available as custom matchers (e.g., in this case `toBeGoofy`).
+   */
+  beforeEach(function() {
+    jasmine.addMatchers(customMatchers);
+  });
+
+  /**
+   * Once a custom matcher is registered with Jasmine, it is available on any expectation.
+   */
+  it("is available on an expectation", function() {
+    expect({
+      hyuk: 'gawrsh'
+    }).toBeGoofy();
+  });
+
+  it("can take an 'expected' parameter", function() {
+    expect({
+      hyuk: 'gawrsh is fun'
+    }).toBeGoofy(' is fun');
+  });
+
+  it("can be negated", function() {
+    expect({
+      hyuk: 'this is fun'
+    }).not.toBeGoofy();
+  });
+});
\ No newline at end of file
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/defaultReporterSpec.js
@@ -0,0 +1,172 @@
+import * as ConsoleReporter from '../src/consoleReporter.js';
+
+describe('Default console reporter', function () {
+    let out, reporter, timerSpies;
+
+    beforeEach(function () {
+        out = (function () {
+            let output = '';
+            return {
+                print(str) {
+                    output += str;
+                },
+                getOutput() {
+                    return output;
+                },
+                clear() {
+                    output = '';
+                },
+            };
+        })();
+
+        timerSpies = {};
+        const timerSpy = id => {
+            timerSpies[id] = jasmine.createSpyObj('timer', ['start', 'elapsed']);
+            return timerSpies[id];
+        };
+
+        reporter = new ConsoleReporter.DefaultReporter({
+            print: out.print,
+            show_colors: false,
+            timerFactory: timerSpy,
+        });
+    });
+
+    it('reports that the suite has started to the console', function () {
+        reporter.jasmineStarted();
+        expect(out.getOutput()).toEqual('Started\n');
+    });
+
+    it('reports a passing spec as a dot', function () {
+        reporter.specDone({status: 'passed'});
+        expect(out.getOutput()).toEqual('.');
+    });
+
+    it('does not report a disabled spec', function () {
+        reporter.specDone({status: 'disabled'});
+        expect(out.getOutput()).toEqual('');
+    });
+
+    it('reports a failing spec as an "F"', function () {
+        reporter.specDone({status: 'failed'});
+        expect(out.getOutput()).toEqual('F');
+    });
+
+    it('reports a pending spec as a "*"', function () {
+        reporter.specDone({status: 'pending'});
+        expect(out.getOutput()).toEqual('*');
+    });
+
+    it('alerts user if there are no specs', function () {
+        reporter.jasmineStarted();
+        out.clear();
+        reporter.jasmineDone();
+        expect(out.getOutput()).toMatch(/No specs found/);
+    });
+
+    it('reports a summary when done (singular spec)', function () {
+        reporter.jasmineStarted();
+        reporter.specStarted({});
+        reporter.specDone({status: 'passed'});
+
+        timerSpies['main'].elapsed.and.returnValue(1000);
+
+        out.clear();
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1 spec, 0 failed/);
+        expect(out.getOutput()).not.toMatch(/0 pending/);
+        expect(out.getOutput()).toMatch('Finished in 1 s\n');
+    });
+
+    it('reports a summary when done (pluralized specs)', function () {
+        reporter.jasmineStarted();
+        reporter.specStarted({});
+        reporter.specDone({status: 'passed'});
+        reporter.specStarted({});
+        reporter.specDone({status: 'pending'});
+        reporter.specStarted({});
+        reporter.specDone({
+            status: 'failed',
+            description: 'with a failing spec',
+            fullName: 'A suite with a failing spec',
+            failedExpectations: [{
+                passed: false,
+                message: 'Expected true to be false.',
+                expected: false,
+                actual: true,
+                stack: 'fakeStack\nfakeStack',
+            }],
+        });
+
+        out.clear();
+
+        timerSpies['main'].elapsed.and.returnValue(100);
+
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/3 specs, 1 failed, 1 pending/);
+        expect(out.getOutput()).toMatch('Finished in 0.1 s\n');
+    });
+
+    it('reports a summary when done that includes the failed spec number before the full name of a failing spec', function () {
+        reporter.jasmineStarted();
+        reporter.specDone({status: 'passed'});
+        reporter.specDone({
+            status: 'failed',
+            description: 'with a failing spec',
+            fullName: 'A suite with a failing spec',
+            failedExpectations: [{
+                passed: false,
+                message: 'Expected true to be false.',
+                expected: false,
+                actual: true,
+                stack: 'fakeStack\nfakeStack',
+            }],
+        });
+
+        out.clear();
+
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1\) A suite with a failing spec/);
+    });
+
+    describe('with color', function () {
+        beforeEach(function () {
+            reporter = new ConsoleReporter.DefaultReporter({
+                print: out.print,
+                showColors: true,
+            });
+        });
+
+        it('reports that the suite has started to the console', function () {
+            reporter.jasmineStarted();
+            expect(out.getOutput()).toEqual('Started\n');
+        });
+
+        it('reports a passing spec as a dot', function () {
+            reporter.specDone({status: 'passed'});
+            expect(out.getOutput()).toEqual('\x1B[32m.\x1B[0m');
+        });
+
+        it('does not report a disabled spec', function () {
+            reporter.specDone({status: 'disabled'});
+            expect(out.getOutput()).toEqual('');
+        });
+
+        it('reports a failing spec as an "F"', function () {
+            reporter.specDone({status: 'failed'});
+            expect(out.getOutput()).toEqual('\x1B[31mF\x1B[0m');
+        });
+    });
+
+    it('displays all afterAll exceptions', function () {
+        reporter.suiteDone({failedExpectations: [{message: 'After All Exception'}]});
+        reporter.suiteDone({failedExpectations: [{message: 'Some Other Exception'}]});
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/After All Exception/);
+        expect(out.getOutput()).toMatch(/Some Other Exception/);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/include.json
@@ -0,0 +1,4 @@
+{
+    "include_paths": "include",
+    "spec_files": "someSpec.js"
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/include/module.js
@@ -0,0 +1,5 @@
+/* exported importedFunction */
+
+function importedFunction() {
+    return true;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/include/spec.js
@@ -0,0 +1,5 @@
+describe('A spec that should not be loaded', function () {
+    it('is not loaded', function () {
+        fail('Spec was loaded when it should not be');
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/jasmine.json
@@ -0,0 +1,4 @@
+{
+    "a": "b",
+    "c": "d"
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/path.json
@@ -0,0 +1,4 @@
+{
+    "include_paths": ".",
+    "spec_files": "someSpec.js"
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/path1/test.js
@@ -0,0 +1 @@
+describe('A suite', function () {});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/path2/test.js
@@ -0,0 +1 @@
+throw new Error('Catch this error to ensure this file is loaded');
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/fixtures/syntaxErrorSpec.js
@@ -0,0 +1,2 @@
+lett foo = 4;
+describe('SyntaxError suite', function () {});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/focusedSpecIntegrationTest.js
@@ -0,0 +1,42 @@
+/* eslint no-restricted-globals: off */
+
+// This is a test of focused suites and specs with the fdescribe() and fit()
+// functions. It's taken from Jasmine's documentation suite:
+//   http://jasmine.github.io/2.2/focused_specs.html
+
+// By definition, this suite will disable all other suites that are run during
+// the same invocation of Jasmine -- so it is skipped by default. To run this
+// test anyway, run it explicitly with "jasmine
+// test/focusedSpecIntegrationTest.js". During "make check" it is still run,
+// since make runs the spec files one by one and so this suite won't interfere
+// with any other suites.
+
+describe('Focused specs', function () {
+    fit('is focused and will run', function () {
+        expect(true).toBeTruthy();
+    });
+
+    it('is not focused and will not run', function () {
+        expect(true).toBeFalsy();
+    });
+
+    fdescribe('focused describe', function () {
+        it('will run', function () {
+            expect(true).toBeTruthy();
+        });
+
+        it('will also run', function () {
+            expect(true).toBeTruthy();
+        });
+    });
+
+    fdescribe('another focused describe', function () {
+        fit('is focused and will run', function () {
+            expect(true).toBeTruthy();
+        });
+
+        it('is not focused and will not run', function () {
+            expect(true).toBeFalsy();
+        });
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/importerSpec.js
@@ -0,0 +1,11 @@
+const Utils = imports.utils;
+
+describe('Jasmine importer', function () {
+    it('hides Jasmine modules from the test code', function () {
+        expect(Object.keys(Utils)).not.toContain('indenter');
+    });
+
+    it("lets test code import modules named the same as Jasmine's", function () {
+        expect(Utils.add).toBeDefined();
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/jasmineBootSpec.js
@@ -0,0 +1,191 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import * as JasmineBoot from '../src/jasmineBoot.js';
+
+const [testFile] = GLib.filename_from_uri(import.meta.url);
+const testDir = GLib.path_get_dirname(testFile);
+
+const customMatchers = {
+    toMatchAllFiles() {
+        return {
+            compare(actual, expected) {
+                const result = {
+                    message: `Expected ${JSON.stringify(actual)} `,
+                };
+                if (actual.length !== expected.length) {
+                    result.pass = false;
+                    result.message += `to match ${expected.length} ${
+                        expected.length === 1 ? 'file' : 'files'
+                    }, but it contained ${actual.length}`;
+                    return result;
+                }
+
+                let unexpectedFile;
+                result.pass = actual.every(path => {
+                    const actualFile = Gio.File.new_for_path(path);
+                    const retval = expected.some(expectedPath => {
+                        const expectedFile = Gio.File.new_for_path(expectedPath);
+                        return actualFile.equal(expectedFile);
+                    });
+                    if (!retval)
+                        unexpectedFile = path;
+                    return retval;
+                });
+                if (result.pass) {
+                    result.message += `not to match a list of files ${
+                        expected}, but it did.`;
+                } else if (unexpectedFile) {
+                    result.message += `to match a list of files ${expected
+                    }, but it contained ${unexpectedFile}`;
+                } else {
+                    result.message += `to match the list of files ${expected
+                    }, but it did not: ${actual}`;
+                }
+                return result;
+            },
+        };
+    },
+};
+
+describe('Jasmine boot', function () {
+    let testJasmine;
+
+    beforeEach(function () {
+        const bootedJasmine = {
+            getEnv: jasmine.createSpy('getEnv').and.returnValue({
+                addReporter: jasmine.createSpy('addReporter'),
+                execute: jasmine.createSpy('execute'),
+                configure: jasmine.createSpy('configure'),
+            }),
+            Timer: jasmine.createSpy('Timer'),
+            Expectation: {
+                addMatchers: jasmine.createSpy('addMatchers'),
+            },
+        };
+
+        const fakeJasmineRequireObj = {
+            core: jasmine.createSpy('core').and.returnValue(bootedJasmine),
+            interface: jasmine.createSpy('interface'),
+        };
+
+        const fakeJasmineCore = {
+            getJasmineRequireObj() {
+                return fakeJasmineRequireObj;
+            },
+            __file__: 'fake/jasmine/path/jasmine.js',
+        };
+
+        testJasmine = new JasmineBoot.Jasmine({jasmineCore: fakeJasmineCore});
+
+        jasmine.addMatchers(customMatchers);
+    });
+
+    it('adds a nonexistent spec file', function () {
+        expect(testJasmine.specFiles).toEqual([]);
+        testJasmine.addSpecFiles([`${testDir}/non/existent/file.js`]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/non/existent/file.js`,
+        ]);
+    });
+
+    it('adds a real spec file', function () {
+        expect(testJasmine.specFiles).toEqual([]);
+        testJasmine.addSpecFiles([`${testDir}/fixtures/someSpec.js`]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/fixtures/someSpec.js`,
+        ]);
+    });
+
+    it('adds more than one spec file', function () {
+        expect(testJasmine.specFiles).toEqual([]);
+        testJasmine.addSpecFiles([
+            `${testDir}/fixtures/someSpec.js`,
+            `${testDir}/fixtures/otherSpec.js`,
+        ]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/fixtures/someSpec.js`,
+            `${testDir}/fixtures/otherSpec.js`,
+        ]);
+    });
+
+    it('adds a whole directory of spec files', function () {
+        expect(testJasmine.specFiles).toEqual([]);
+        testJasmine.addSpecFiles([`${testDir}/fixtures`]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/fixtures/include/module.js`,
+            `${testDir}/fixtures/include/spec.js`,
+            `${testDir}/fixtures/otherSpec.js`,
+            `${testDir}/fixtures/path1/test.js`,
+            `${testDir}/fixtures/path2/test.js`,
+            `${testDir}/fixtures/someSpec.js`,
+            `${testDir}/fixtures/syntaxErrorSpec.js`,
+        ]);
+        expect(testJasmine.specFiles.every(path => path.indexOf('notASpec.txt') === -1)).toBe(true);
+    });
+
+    it('adds spec files in different directories with the same name', function () {
+        testJasmine.addSpecFiles([
+            `${testDir}/fixtures/path1`,
+            `${testDir}/fixtures/path2`,
+        ]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/fixtures/path1/test.js`,
+            `${testDir}/fixtures/path2/test.js`,
+        ]);
+    });
+
+    it('respects excluded files', function () {
+        testJasmine.exclusions = ['otherSpec.js', 'syntaxErrorSpec.js'];
+        testJasmine.addSpecFiles([`${testDir}/fixtures`]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/fixtures/include/module.js`,
+            `${testDir}/fixtures/include/spec.js`,
+            `${testDir}/fixtures/someSpec.js`,
+            `${testDir}/fixtures/path1/test.js`,
+            `${testDir}/fixtures/path2/test.js`,
+        ]);
+    });
+
+    it('matches at the end of the containing path', function () {
+        testJasmine.exclusions = ['test/fixtures'];
+        testJasmine.addSpecFiles([`${testDir}/fixtures`]);
+        expect(testJasmine.specFiles).toMatchAllFiles([
+            `${testDir}/fixtures/include/module.js`,
+            `${testDir}/fixtures/include/spec.js`,
+            `${testDir}/fixtures/path1/test.js`,
+            `${testDir}/fixtures/path2/test.js`,
+        ]);
+    });
+
+    it('can handle globs in excluded files', function () {
+        testJasmine.exclusions = ['*.js'];
+        testJasmine.addSpecFiles([`${testDir}/fixtures`]);
+        expect(testJasmine.specFiles).toMatchAllFiles([]);
+    });
+
+    it('adds the Jasmine path when adding a reporter', function () {
+        const fakeReporter = {};
+        testJasmine.addReporter(fakeReporter);
+        expect(fakeReporter.jasmine_core_path).toMatch('fake/jasmine/path');
+    });
+
+    it('imports spec files in different directories with the same name', function () {
+        testJasmine.addSpecFiles([
+            `${testDir}/fixtures/path1`,
+            `${testDir}/fixtures/path2`,
+        ]);
+        expectAsync(testJasmine.loadSpecs()).toBeRejectedWithError(Error,
+            'Catch this error to ensure this file is loaded');
+    });
+
+    it('does not bail out altogether if one of the specs has a syntax error', function () {
+        testJasmine.addSpecFiles([`${testDir}/fixtures/syntaxErrorSpec.js`]);
+        expectAsync(testJasmine.loadSpecs()).toBeResolved();
+    });
+
+    it('does not bail out altogether if one of the specs does not exist', function () {
+        testJasmine.addSpecFiles(['non/existent/file.js']);
+        expectAsync(testJasmine.loadSpecs()).toBeResolved();
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/junitReporterSpec.js
@@ -0,0 +1,470 @@
+import GLib from 'gi://GLib';
+
+import * as JUnitReporter from '../src/junitReporter.js';
+import * as XMLWriter from '../src/xmlWriter.js';
+
+const SUITE_INFO = {
+    id: 'foo',
+    description: 'A suite',
+    fullName: 'A suite',
+    failedExpectations: [],
+    status: 'finished',
+};
+const NESTED_SUITE_INFO = {
+    id: 'baz',
+    description: 'nested',
+    fullName: 'A suite nested',
+    failedExpectations: [],
+    status: 'finished',
+};
+const FAILED_SUITE_INFO = {
+    id: 'cheers',
+    description: 'A failing suite',
+    fullName: 'A failing suite',
+    failedExpectations: [{
+        matcherName: '',
+        message: 'Some error',
+        stack: 'file.js:113\nfile.js:72\nfile.js:17\n',
+    }],
+    status: 'failed',
+};
+const PASSING_SPEC_INFO = {
+    id: 'bar',
+    description: 'passes a test',
+    fullName: 'A suite passes a test',
+    failedExpectations: [],
+    passedExpectations: [
+        {
+            matcherName: 'toBe',
+            message: 'Expected true to be true',
+        },
+        {
+            matcherName: 'toContain',
+            message: 'Expected [1] to contain 1',
+        },
+    ],
+    status: 'passed',
+};
+const NESTED_PASSING_SPEC_INFO = {
+    id: 'boz',
+    description: 'passes a test',
+    fullName: 'A suite nested passes a test',
+    failedExpectations: [],
+    passedExpectations: [],
+    status: 'passed',
+};
+const PENDING_SPEC_INFO = {
+    id: 'skip',
+    description: 'skips a test',
+    fullName: 'A suite skips a test',
+    failedExpectations: [],
+    passedExpectations: [],
+    status: 'pending',
+};
+const FAILING_SPEC_INFO = {
+    id: 'bad',
+    description: 'fails a test',
+    fullName: 'A suite fails a test',
+    failedExpectations: [{
+        matcherName: 'toBe',
+        message: 'Expected true to be false',
+        stack: 'file.js:113\nfile.js:72\nfile.js:17\n',
+    }],
+    passedExpectations: [],
+    status: 'failed',
+};
+const ERROR_SPEC_INFO = {
+    id: 'bug',
+    description: 'has a bug in a test',
+    fullName: 'A suite has a bug in a test',
+    failedExpectations: [
+        {
+            matcherName: '',
+            message: 'TypeError: foo is not a function',
+            stack: 'file.js:113\nfile.js:72\nfile.js:17\n',
+        },
+        {
+            matcherName: '',
+            message: 'Some other unknown error',
+            stack: 'file.js:113\nfile.js:72\nfile.js:17\n',
+        },
+    ],
+    passedExpectations: [],
+    status: 'failed',
+};
+const DISABLED_SPEC_INFO = {
+    id: 'wut',
+    description: 'disables a test',
+    fullName: 'A suite disables a test',
+    failedExpectations: [],
+    passedExpectations: [],
+    status: 'disabled',
+};
+
+describe('The JUnit reporter', function () {
+    let out, reporter, timerSpies;
+
+    beforeEach(function () {
+        // Override the XML outputting function to output JSON instead. This is
+        // for ease of verifying the output. XML is inconvenient to parse in the
+        // DOM-less GJS. Any regressions in the XML output should not be tested
+        // here, but instead should be covered in xmlWriterSpec.js.
+        spyOn(XMLWriter.Node.prototype, 'toString').and.callFake(function () {
+            return JSON.stringify(this);
+        });
+
+        out = (function () {
+            let output = '';
+            return {
+                print(str) {
+                    output += str;
+                },
+                getOutput() {
+                    return output;
+                },
+                clear() {
+                    output = '';
+                },
+            };
+        })();
+
+        timerSpies = {};
+        const timerSpy = id => {
+            timerSpies[id] = jasmine.createSpyObj('timer', ['start', 'elapsed']);
+            return timerSpies[id];
+        };
+
+        reporter = new JUnitReporter.JUnitReporter({
+            print: out.print,
+            timerFactory: timerSpy,
+        });
+        reporter.jasmineStarted();
+    });
+
+    function runSpec(specInfo) {
+        reporter.specStarted(specInfo);
+        reporter.specDone(specInfo);
+    }
+
+    function runSuite(suiteInfo, specs) {
+        reporter.suiteStarted(suiteInfo);
+        specs.forEach(runSpec);
+        reporter.suiteDone(suiteInfo);
+    }
+
+    // Find the <testsuite> element with the given ID inside the <testsuites>
+    // element given by tree. This is necessary because other elements may be
+    // present such as, <properties>, so we cannot rely on the element with ID 0
+    // being the first child of <testsuites>.
+    function findSuite(tree, id) {
+        for (let index = 0; index < tree.children.length; index++) {
+            const child = tree.children[index];
+            if (child.name === 'testsuite' && child.attrs['id'] === id)
+                return child;
+        }
+        return undefined;
+    }
+
+    // For XML builder reasons, the report is only output at the end of all the
+    // test suites. Therefore all tests must call jasmineDone().
+
+    it('outputs a JUnit report', function () {
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        expect(tree.name).toBe('testsuites');
+    });
+
+    it('reports all required elements of a test suite', function () {
+        runSuite(SUITE_INFO, []);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.name).toBe('testsuite');
+        expect(testsuite.attrs['name']).toBe('A suite');
+        expect(testsuite.attrs['tests']).toBe(0);
+    });
+
+    it('reports all required elements of a test case element', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        const [{name, attrs}] = testsuite.children;
+        expect(name).toBe('testcase');
+        expect(attrs['name']).toBe('passes a test');
+        expect(attrs['classname']).toBe('A suite');
+    });
+
+    it('reports a pending spec as skipped', function () {
+        runSuite(SUITE_INFO, [PENDING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        const [{children}] = testsuite.children;
+        expect(children[0].name).toBe('skipped');
+    });
+
+    describe('given a spec with a failed expectation', function () {
+        let failure;
+
+        beforeEach(function () {
+            runSuite(SUITE_INFO, [FAILING_SPEC_INFO]);
+            reporter.jasmineDone();
+
+            const tree = JSON.parse(out.getOutput());
+            const testsuite = findSuite(tree, 0);
+            [failure] = testsuite.children[0].children;
+        });
+
+        it('reports it as failed', function () {
+            expect(failure.name).toBe('failure');
+        });
+
+        it('reports the matcher name as the failure type', function () {
+            if (!failure.attrs.hasOwnProperty('type'))
+                pending();
+            expect(failure.attrs['type']).toBe('toBe');
+        });
+
+        it('reports the expectation message', function () {
+            if (!failure.attrs.hasOwnProperty('message'))
+                pending();
+            expect(failure.attrs['message']).toBe('Expected true to be false');
+        });
+
+        it('reports the stack trace', function () {
+            expect(failure.text).toBe('file.js:113\nfile.js:72\nfile.js:17\n');
+        });
+    });
+
+    describe('given a spec with an uncaught exception', function () {
+        let error1, error2;
+
+        beforeEach(function () {
+            runSuite(SUITE_INFO, [ERROR_SPEC_INFO]);
+            reporter.jasmineDone();
+
+            const tree = JSON.parse(out.getOutput());
+            const testsuite = findSuite(tree, 0);
+            [error1, error2] = testsuite.children[0].children;
+        });
+
+        it('reports it as errored', function () {
+            expect(error1.name).toBe('error');
+            expect(error2.name).toBe('error');
+        });
+
+        it('reports the exception class as the failure type', function () {
+            expect(error1.attrs['type']).toBe('TypeError');
+        });
+
+        it('picks a default type if the exception class is not known', function () {
+            expect(error2.attrs['type']).toBe('Error');
+        });
+
+        it('reports the error message', function () {
+            expect(error1.attrs['message']).toBe('TypeError: foo is not a function');
+            expect(error2.attrs['message']).toBe('Some other unknown error');
+        });
+
+        it('reports the stack trace', function () {
+            expect(error1.text).toBe('file.js:113\nfile.js:72\nfile.js:17\n');
+            expect(error2.text).toBe('file.js:113\nfile.js:72\nfile.js:17\n');
+        });
+    });
+
+    it('does not report a disabled spec', function () {
+        runSuite(SUITE_INFO, [DISABLED_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.children.length).toBe(0);
+    });
+
+    it('gives each suite an increasing ID number', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO]);
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO]);
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        tree.children.filter(child => child.name === 'testsuite')
+        .forEach((child, index) => {
+            expect(child.attrs['id']).toBe(index);
+        });
+    });
+
+    it('times all suites together', function () {
+        timerSpies['main'].elapsed.and.returnValue(1200);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        expect(tree.attrs['time']).toBeCloseTo(1.2, 4);
+    });
+
+    it('times individual suites', function () {
+        reporter.suiteStarted(SUITE_INFO);
+        timerSpies[`suite:${SUITE_INFO.id}`].elapsed.and.returnValue(100);
+        reporter.suiteDone(SUITE_INFO);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.attrs['time']).toBeCloseTo(0.1, 4);
+    });
+
+    it('times individual specs', function () {
+        reporter.suiteStarted(SUITE_INFO);
+        reporter.specStarted(PASSING_SPEC_INFO);
+        timerSpies[`spec:${PASSING_SPEC_INFO.id}`].elapsed.and.returnValue(100);
+        reporter.specDone(PASSING_SPEC_INFO);
+        reporter.suiteDone(SUITE_INFO);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        const [{attrs}] = testsuite.children;
+        expect(attrs['time']).toBeCloseTo(0.1, 4);
+    });
+
+    it('counts all tests in a suite', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO, PASSING_SPEC_INFO, PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.attrs['tests']).toBe(3);
+    });
+
+    it('counts disabled tests in a suite', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO, DISABLED_SPEC_INFO, PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.attrs['disabled']).toBe(1);
+    });
+
+    it('counts errored tests in a suite', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO, ERROR_SPEC_INFO, PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.attrs['errors']).toBe(1);
+    });
+
+    it('counts failed tests in a suite', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO, FAILING_SPEC_INFO, PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.attrs['failures']).toBe(1);
+    });
+
+    it('counts skipped tests in a suite', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO, PENDING_SPEC_INFO, PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.attrs['skipped']).toBe(1);
+    });
+
+    it('timestamps a suite', function () {
+        runSuite(SUITE_INFO, []);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(() => Date.parse(testsuite.attrs['timestamp'])).not.toThrow();
+    });
+
+    it('flattens nested suites', function () {
+        reporter.suiteStarted(SUITE_INFO);
+        [PASSING_SPEC_INFO, PASSING_SPEC_INFO].forEach(runSpec);
+        reporter.suiteStarted(NESTED_SUITE_INFO);
+        [NESTED_PASSING_SPEC_INFO, NESTED_PASSING_SPEC_INFO].forEach(runSpec);
+        reporter.suiteDone(NESTED_SUITE_INFO);
+        runSpec(PASSING_SPEC_INFO);
+        reporter.suiteDone(SUITE_INFO);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const suite1 = findSuite(tree, 0);
+        const suite2 = findSuite(tree, 1);
+        expect(suite1.attrs['tests']).toBe(3);
+        expect(suite2.attrs['tests']).toBe(2);
+    });
+
+    it('reports exceptions in afterAll() as errors in a separate suite', function () {
+        runSuite(FAILED_SUITE_INFO, [PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const afterAllSuite = findSuite(tree, 1);
+        expect(afterAllSuite.attrs['tests']).toBe(1);
+        expect(afterAllSuite.attrs['errors']).toBe(1);
+    });
+
+    it('reports an error in afterAll() as a test case', function () {
+        runSuite(FAILED_SUITE_INFO, [PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const afterAllSuite = findSuite(tree, 1);
+        expect(afterAllSuite.children[0]).toEqual(jasmine.objectContaining({
+            name: 'testcase',
+            attrs: jasmine.any(Object),
+            children: [jasmine.objectContaining({
+                name: 'error',
+                attrs: jasmine.objectContaining({
+                    message: 'Some error',
+                    type: 'Error',
+                }),
+                text: 'file.js:113\nfile.js:72\nfile.js:17\n',
+            })],
+        }));
+    });
+
+    it('adds the environment in a <properties> element', function () {
+        GLib.setenv('JASMINE_TESTS_BOGUS_VARIABLE', 'surprise', true);
+        reporter.jasmineStarted(); // restart
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const [properties] = tree.children.filter(child => child.name === 'properties');
+        expect(properties.children).toContain(jasmine.objectContaining({
+            name: 'property',
+            attrs: {
+                name: 'JASMINE_TESTS_BOGUS_VARIABLE',
+                value: 'surprise',
+            },
+        }));
+    });
+
+    it('reports the total number of assertions', function () {
+        runSuite(SUITE_INFO, [PASSING_SPEC_INFO, ERROR_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const testsuite = findSuite(tree, 0);
+        expect(testsuite.children[0].attrs['assertions']).toBe(2);
+        expect(testsuite.children[1].attrs['assertions']).toBe(2);
+    });
+
+    it('reports the total number of assertions in an afterAll() suite', function () {
+        runSuite(FAILED_SUITE_INFO, [PASSING_SPEC_INFO]);
+        reporter.jasmineDone();
+
+        const tree = JSON.parse(out.getOutput());
+        const afterAllSuite = findSuite(tree, 1);
+        expect(afterAllSuite.children[0].attrs['assertions']).toBe(1);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/optionsSpec.js
@@ -0,0 +1,47 @@
+// FIXME: the argument parser should be refactored into something more general
+// so that the ARGS object in options.js isn't hardcoded.
+
+import * as Options from '../src/options.js';
+
+describe('Argument parser', function () {
+    it('stores the given value for a "store" argument', function () {
+        const [files, namespace] = Options.parseOptions(['--junit', 'file.txt']);
+        expect(files).toEqual([]);
+        expect(namespace['junit']).toEqual('file.txt');
+    });
+
+    it('stores the value from "const" for a "store" argument if none given', function () {
+        const [files, namespace] = Options.parseOptions(['--junit']);
+        expect(files).toEqual([]);
+        expect(namespace['junit']).toEqual('report.xml');
+    });
+
+    it('stores values in the order they are given on the command line', function () {
+        const [, namespace] = Options.parseOptions(['--no-color', '--color']);
+        expect(namespace['color']).toBe(true);
+    });
+
+    it('stores the given value for an "append" argument', function () {
+        const [, namespace] = Options.parseOptions(['--exclude', 'file.js']);
+        expect(namespace['exclude']).toEqual(['file.js']);
+    });
+
+    it('stores multiple values for an "append" argument appearing multiple times', function () {
+        const [, namespace] = Options.parseOptions([
+            '--exclude', 'file.js',
+            '--exclude', 'file2.js',
+        ]);
+        expect(namespace['exclude']).toEqual(['file.js', 'file2.js']);
+    });
+
+    it('does not modify the list of arguments passed in', function () {
+        const args = ['arg1', 'arg2', 'arg3'];
+        Options.parseOptions(args);
+        expect(args.length).toBe(3);
+    });
+
+    it('does not treat a short option as a file', function () {
+        const [files] = Options.parseOptions(['-c', 'file.js']);
+        expect(files).toEqual(['file.js']);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/tapReporterSpec.js
@@ -0,0 +1,251 @@
+import * as TapReporter from '../src/tapReporter.js';
+
+describe('The TAP reporter', function () {
+    let out, reporter;
+
+    beforeEach(function () {
+        out = (function () {
+            let output = '';
+            return {
+                print(str) {
+                    output += str;
+                },
+                getOutput() {
+                    return output;
+                },
+                clear() {
+                    output = '';
+                },
+            };
+        })();
+
+        reporter = new TapReporter.TapReporter({
+            print: out.print,
+        });
+    });
+
+    it('outputs a test plan', function () {
+        reporter.jasmineStarted({totalSpecsDefined: 1});
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'passed',
+        });
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1../);
+    });
+
+    it('includes the total number of specs in the test plan', function () {
+        reporter.jasmineStarted({totalSpecsDefined: 2});
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'passed',
+        });
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'bar',
+            status: 'passed',
+        });
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1..2/);
+    });
+
+    it('outputs an empty test plan if there were no specs', function () {
+        reporter.jasmineStarted({totalSpecsDefined: 0});
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1..0/);
+    });
+
+    it('outputs a line starting with "ok" for a passing spec', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'passed',
+        });
+        expect(out.getOutput()).toMatch(/^ok/);
+    });
+
+    it('outputs a line starting with "not ok" for a failing spec', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'failed',
+        });
+        expect(out.getOutput()).toMatch(/^not ok/);
+    });
+
+    it('outputs an "ok" line plus a skip directive for a pending spec', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'pending',
+        });
+        expect(out.getOutput()).toMatch(/^ok/);
+        expect(out.getOutput()).toMatch(/# skip/i);
+    });
+
+    it('reports the reason for a pending spec, if given', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'pending',
+            pendingReason: 'because I said so',
+        });
+        expect(out.getOutput()).toMatch('because I said so');
+    });
+
+    it('outputs an "ok" line plus a skip directive for a disabled spec', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'disabled',
+        });
+        expect(out.getOutput()).toMatch(/^ok/);
+        expect(out.getOutput()).toMatch(/# skip/i);
+    });
+
+    it('outputs a sequence number after the result', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'foo',
+            status: 'passed',
+        });
+        expect(out.getOutput()).toMatch(/^ok 1/);
+        out.clear();
+        reporter.specStarted({});
+        reporter.specDone({
+            fullName: 'bar',
+            status: 'failed',
+        });
+        expect(out.getOutput()).toMatch(/^not ok 2/);
+    });
+
+    it('outputs the name of the spec on the test line', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            status: 'passed',
+            fullName: 'A passing spec',
+        });
+        expect(out.getOutput()).toMatch(/^ok.*A passing spec/);
+        out.clear();
+        reporter.specStarted({});
+        reporter.specDone({
+            status: 'failed',
+            fullName: 'A failing spec',
+        });
+        expect(out.getOutput()).toMatch(/^not ok.*A failing spec/);
+    });
+
+    it('does not let the name start with a digit', function () {
+        reporter.specStarted({});
+        reporter.specDone({
+            status: 'passed',
+            fullName: '3 careless programmers wrote this',
+        });
+        expect(out.getOutput()).toMatch(/^ok 1/);
+        expect(out.getOutput()).not.toMatch(/^ok 1\s*[0-9]+/);
+    });
+
+    describe('on failure', function () {
+        beforeEach(function () {
+            reporter.specStarted({});
+            out.clear();
+            reporter.specDone({
+                fullName: 'foo',
+                status: 'failed',
+                failedExpectations: [
+                    {
+                        message: 'Expected the earth and the sky.',
+                        stack: 'line 1\nline 2\nline 3',
+                    },
+                    {
+                        message: 'Expectations exceeded.',
+                        stack: 'line 4\nline 5',
+                    },
+                ],
+            });
+        });
+
+        it('outputs messages on the test line', function () {
+            const output = out.getOutput();
+            const [testLine] = output.split('\n');
+            expect(testLine).toMatch(/^not ok.*Expected the earth and the sky\./);
+            expect(testLine).toMatch(/^not ok.*Expectations exceeded\./);
+        });
+
+        it('outputs messages and stack traces as diagnostic lines', function () {
+            const diagnostics = out.getOutput().split('\n').slice(1);
+            expect(diagnostics).toMatch(/^#.*Expected the earth and the sky\./m);
+            expect(diagnostics).toMatch(/^#.*Expectations exceeded\./m);
+            expect(diagnostics).toMatch(/^#.*line 1/m);
+            expect(diagnostics).toMatch(/^#.*line 2/m);
+            expect(diagnostics).toMatch(/^#.*line 3/m);
+            expect(diagnostics).toMatch(/^#.*line 4/m);
+            expect(diagnostics).toMatch(/^#.*line 5/m);
+        });
+    });
+
+    describe('on failure with newlines', function () {
+        beforeEach(function () {
+            reporter.specStarted({});
+            out.clear();
+            reporter.specDone({
+                fullName: 'foo',
+                status: 'failed',
+                failedExpectations: [{
+                    message: 'A message\non two lines',
+                    stack: '',
+                }],
+            });
+        });
+
+        it('prints no newlines on the test line', function () {
+            const [testLine] = out.getOutput().split('\n');
+            expect(testLine).toMatch(/^not ok.*A message.*on two lines/);
+        });
+
+        it('prints no newlines in the diagnostics', function () {
+            const diagnostics = out.getOutput().split('\n').slice(1);
+            expect(diagnostics).not.toMatch(/^[^#]/);
+        });
+    });
+
+    it('reports suites starting as diagnostic lines', function () {
+        reporter.suiteStarted({
+            fullName: 'A suite',
+        });
+        expect(out.getOutput()).toMatch(/^#.*A suite/);
+    });
+
+    it('reports suites finishing successfully as diagnostic lines', function () {
+        reporter.suiteDone({
+            status: 'passed',
+            fullName: 'A suite',
+            failedExpectations: [],
+        });
+        expect(out.getOutput()).toMatch(/^#.*A suite/);
+    });
+
+    it('reports suites with failing specs as diagnostic lines', function () {
+        reporter.suiteDone({
+            status: 'failed',
+            fullName: 'A suite',
+            failedExpectations: [],
+        });
+        expect(out.getOutput()).toMatch(/^#.*A suite/);
+    });
+
+    it('reports failures in afterAll as an extra failure', function () {
+        reporter.suiteDone({
+            status: 'failed',
+            fullName: 'A suite',
+            failedExpectations: [{message: 'An afterAll exception'}],
+        });
+        reporter.jasmineDone();
+        expect(out.getOutput()).toMatch(/^not ok.*An afterAll exception/m);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/timerSpec.js
@@ -0,0 +1,9 @@
+import * as Timer from '../src/timer.js';
+
+describe('The default timer', function () {
+    it('stops timing when elapsed() is called', function () {
+        const timer = Timer.createDefaultTimer();
+        timer.start();
+        expect(timer.elapsed()).toEqual(timer.elapsed());
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/utils.js
@@ -0,0 +1,3 @@
+function add(a, b) {
+    return a + b;
+}
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/utilsSpec.js
@@ -0,0 +1,20 @@
+import {indenter} from '../src/utils.js';
+
+describe('Indent', function () {
+    it('prepends spaces to a string', function () {
+        expect(indenter.indent('foo', 4)).toEqual('    foo');
+    });
+
+    it('prepends spaces to each line in a string', function () {
+        expect(indenter.indent('a\nb\nc', 4)).toEqual('    a\n    b\n    c');
+    });
+
+    it('does not indent an extra blank line at the end of the string', function () {
+        expect(indenter.indent('a\nb\n', 4)).toEqual('    a\n    b\n');
+    });
+
+    it('handles zero spaces', function () {
+        expect(indenter.indent('foo', 0)).toEqual('foo');
+        expect(indenter.indent('a\nb\nc', 0)).toEqual('a\nb\nc');
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/verboseReporterSpec.js
@@ -0,0 +1,307 @@
+import * as VerboseReporter from '../src/verboseReporter.js';
+import {indenter} from '../src/utils.js';
+
+describe('Verbose console reporter', function () {
+    let out, reporter, timerSpy, timerSpies;
+
+    beforeEach(function () {
+        out = (function () {
+            let output = '';
+            return {
+                print(str) {
+                    output += str;
+                },
+                getOutput() {
+                    return output;
+                },
+                clear() {
+                    output = '';
+                },
+            };
+        })();
+
+        timerSpies = {};
+        timerSpy = id => {
+            timerSpies[id] = jasmine.createSpyObj('timer', ['start', 'elapsed']);
+            return timerSpies[id];
+        };
+
+        reporter = new VerboseReporter.VerboseReporter({
+            print: out.print,
+            show_colors: false,
+            timerFactory: timerSpy,
+        });
+
+        // disable indentation for test purposes
+        spyOn(indenter, 'indent').and.callFake(str => str);
+    });
+
+    it('reports that the suite has started to the console', function () {
+        reporter.jasmineStarted();
+        expect(out.getOutput()).toEqual('Started\n\n');
+    });
+
+    it('reports a passing spec with a checkmark', function () {
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            status: 'passed',
+            description: 'A passing spec',
+        });
+        expect(out.getOutput()).toEqual('✓ A passing spec\n');
+    });
+
+    it('reports a disabled spec with an "x"', function () {
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            status: 'disabled',
+            description: 'A disabled spec',
+        });
+        expect(out.getOutput()).toEqual('x A disabled spec\n');
+    });
+
+    it('reports a failing spec with a number', function () {
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            status: 'failed',
+            description: 'A failing spec',
+        });
+        expect(out.getOutput()).toEqual('1) A failing spec\n');
+    });
+
+    it('reports a failing spec with the number of that failure in sequence', function () {
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            status: 'failed',
+            description: 'A failing spec',
+        });
+        out.clear();
+        reporter.specStarted({id: 'bar'});
+        reporter.specDone({
+            id: 'bar',
+            status: 'failed',
+            description: 'Another failing spec',
+        });
+        expect(out.getOutput()).toEqual('2) Another failing spec\n');
+    });
+
+    it('reports a pending spec as a dash', function () {
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            status: 'pending',
+            description: 'A pending spec',
+        });
+        expect(out.getOutput()).toEqual('- A pending spec\n');
+    });
+
+    it('reports a summary when done', function () {
+        reporter.jasmineStarted();
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            description: 'A spec',
+            status: 'passed',
+        });
+        reporter.specStarted({id: 'bar'});
+        reporter.specDone({
+            id: 'bar',
+            description: 'A spec',
+            status: 'pending',
+        });
+        reporter.specStarted({id: 'baz'});
+        reporter.specDone({
+            id: 'baz',
+            status: 'failed',
+            description: 'with a failing spec',
+            fullName: 'A suite with a failing spec',
+            failedExpectations: [{
+                passed: false,
+                message: 'Expected true to be false.',
+                expected: false,
+                actual: true,
+                stack: 'fakeStack\nfakeStack',
+            }],
+        });
+
+        out.clear();
+
+        timerSpies['main'].elapsed.and.returnValue(100);
+
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1 passing \(0.1 s\)/);
+        expect(out.getOutput()).toMatch(/1 pending/);
+        expect(out.getOutput()).toMatch(/1 failing/);
+    });
+
+    it('reports a summary when done even if there are no specs', function () {
+        reporter.jasmineStarted();
+        timerSpies['main'].elapsed.and.returnValue(100);
+        out.clear();
+        reporter.jasmineDone();
+        expect(out.getOutput()).toMatch(/0 passing \(0.1 s\)/);
+    });
+
+    it('reports a summary when done that includes the failed spec number before the full name of a failing spec', function () {
+        reporter.jasmineStarted();
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            description: 'A spec',
+            status: 'passed',
+        });
+        reporter.specStarted({id: 'bar'});
+        reporter.specDone({
+            id: 'bar',
+            status: 'failed',
+            description: 'with a failing spec',
+            fullName: 'A suite with a failing spec',
+            failedExpectations: [{
+                passed: false,
+                message: 'Expected true to be false.',
+                expected: false,
+                actual: true,
+                stack: 'fakeStack\nfakeStack',
+            }],
+        });
+
+        out.clear();
+
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/1\) A suite with a failing spec/);
+    });
+
+    it('prints a warning when a spec takes over 40 ms', function () {
+        reporter.specStarted({id: 'foo'});
+        timerSpies['spec:foo'].elapsed.and.returnValue(50);
+        reporter.specDone({
+            id: 'foo',
+            description: 'A spec',
+            status: 'passed',
+        });
+
+        expect(out.getOutput()).toMatch('(50 ms)');
+    });
+
+    it('prints the reason for a pending spec', function () {
+        reporter.specStarted({id: 'foo'});
+        reporter.specDone({
+            id: 'foo',
+            status: 'pending',
+            description: 'a pending spec',
+            pendingReason: 'it was not ready',
+        });
+        expect(out.getOutput()).toMatch('(it was not ready)');
+    });
+
+    describe('with color', function () {
+        beforeEach(function () {
+            reporter = new VerboseReporter.VerboseReporter({
+                print: out.print,
+                showColors: true,
+                timerFactory: timerSpy,
+            });
+        });
+
+        it('reports that the suite has started to the console', function () {
+            reporter.jasmineStarted();
+            expect(out.getOutput()).toEqual('Started\n\n');
+        });
+
+        it('reports a passing spec with a checkmark', function () {
+            reporter.specStarted({id: 'foo'});
+            reporter.specDone({
+                id: 'foo',
+                status: 'passed',
+                description: 'A passing spec',
+            });
+            expect(out.getOutput()).toEqual('\x1b[32m✓\x1b[0m A passing spec\n');
+        });
+
+        it('reports a disabled spec with an "x"', function () {
+            reporter.specStarted({id: 'foo'});
+            reporter.specDone({
+                id: 'foo',
+                status: 'disabled',
+                description: 'A disabled spec',
+            });
+            expect(out.getOutput()).toEqual('x A disabled spec\n');
+        });
+
+        it('reports a failing spec with a number', function () {
+            reporter.specStarted({id: 'foo'});
+            reporter.specDone({
+                id: 'foo',
+                status: 'failed',
+                description: 'A failing spec',
+            });
+            expect(out.getOutput()).toEqual('\x1b[31m1)\x1b[0m A failing spec\n');
+        });
+
+        it('reports a disabled suite with "disabled"', function () {
+            reporter.suiteDone({
+                status: 'disabled',
+                description: 'A disabled suite',
+            });
+            expect(out.getOutput()).toEqual('\x1b[33m(disabled)\x1b[0m\n');
+        });
+
+        it('prints a mild warning when a spec takes over 40 ms', function () {
+            reporter.specStarted({id: 'foo'});
+            timerSpies['spec:foo'].elapsed.and.returnValue(50);
+            reporter.specDone({
+                id: 'foo',
+                description: 'A spec',
+                status: 'passed',
+            });
+
+            // eslint-disable-next-line no-control-regex
+            expect(out.getOutput()).toMatch(/\x1b\[33m\(50 ms\)\x1b\[0m/);
+        });
+
+        it('prints a loud warning when a spec takes over 75 ms', function () {
+            reporter.specStarted({id: 'foo'});
+            timerSpies['spec:foo'].elapsed.and.returnValue(80);
+            reporter.specDone({
+                id: 'foo',
+                description: 'A spec',
+                status: 'passed',
+            });
+
+            // eslint-disable-next-line no-control-regex
+            expect(out.getOutput()).toMatch(/\x1b\[31m\(80 ms\)\x1b\[0m/);
+        });
+
+        it('prints a pending reason in yellow', function () {
+            reporter.specStarted({id: 'foo'});
+            reporter.specDone({
+                id: 'foo',
+                status: 'pending',
+                description: 'a pending spec',
+                pendingReason: 'it was not ready',
+            });
+            // eslint-disable-next-line no-control-regex
+            expect(out.getOutput()).toMatch(/\x1b\[33m\(it was not ready\)\x1b\[0m/);
+        });
+    });
+
+    it('displays all afterAll exceptions', function () {
+        reporter.suiteDone({
+            status: 'failed',
+            failedExpectations: [{message: 'After All Exception'}],
+        });
+        reporter.suiteDone({
+            status: 'failed',
+            failedExpectations: [{message: 'Some Other Exception'}],
+        });
+        reporter.jasmineDone();
+
+        expect(out.getOutput()).toMatch(/After All Exception/);
+        expect(out.getOutput()).toMatch(/Some Other Exception/);
+    });
+});
--- /dev/null
+++ gnome-shell-48.0/subprojects/jasmine-gjs/test/xmlWriterSpec.js
@@ -0,0 +1,98 @@
+import * as XMLWriter from '../src/xmlWriter.js';
+
+describe('XML writer', function () {
+    let node;
+
+    beforeEach(function () {
+        node = new XMLWriter.Node('node');
+    });
+
+    it('prints a doctype declaration', function () {
+        expect(node.toString()).toMatch(/^<\?xml version=(['"])1\.0\1 encoding=(['"])UTF-8\2\?>/);
+    });
+
+    it('prints a single empty node', function () {
+        expect(node.toString()).toMatch('<node/>');
+    });
+
+    it('prints a single node with an attribute', function () {
+        node.attrs = {
+            attr: 'value',
+        };
+        expect(node.toString()).toMatch(/<node attr=(['"])value\1\/>/);
+    });
+
+    it('separates attributes with a space', function () {
+        node.attrs = {
+            attr: 'value',
+            key: 'something',
+        };
+        expect(node.toString()).toMatch(/<node attr=(['"])value\1 key=(['"])something\2\/>/);
+    });
+
+    it('escapes attribute values', function () {
+        node.attrs = {
+            attr: '"<>&\'',
+        };
+        expect(node.toString()).toMatch(/<node attr=(['"])&quot;&lt;&gt;&amp;&apos;\1\/>/);
+    });
+
+    it('prints child nodes', function () {
+        node.children.push(new XMLWriter.Node('child'));
+        expect(node.toString()).toMatch(/<node>\s*<child\/>\s*<\/node>/);
+    });
+
+    it('prints child nodes in order', function () {
+        node.children = [
+            new XMLWriter.Node('child-one'),
+            new XMLWriter.Node('child-two'),
+            new XMLWriter.Node('child-three'),
+        ];
+        expect(node.toString()).toMatch(/<node>\s*<child-one\/>\s*<child-two\/>\s*<child-three\/>\s*<\/node>/);
+    });
+
+    it('prints child nodes indented', function () {
+        node.children.push(new XMLWriter.Node('child'));
+        expect(node.toString()).toMatch('<node>\n  <child/>\n</node>\n');
+    });
+
+    it('prints multiple levels of indentation', function () {
+        const child = new XMLWriter.Node('child');
+        child.children.push(new XMLWriter.Node('descendant'));
+        node.children.push(child);
+        const output = node.toString();
+        expect(output).toMatch('\n<node>\n');
+        expect(output).toMatch('\n  <child>\n');
+        expect(output).toMatch('\n    <descendant/>\n');
+        expect(output).toMatch('\n  </child>\n');
+        expect(output).toMatch('\n</node>\n');
+    });
+
+    it('prints text content', function () {
+        node.text = 'A very fine day';
+        expect(node.toString()).toMatch(/<node>\s*A very fine day\s*<\/node>/);
+    });
+
+    it('prints text content indented', function () {
+        node.text = 'A very fine day';
+        expect(node.toString()).toMatch('\n  A very fine day\n');
+    });
+
+    it('trims text content', function () {
+        node.text = '     \nA very fine day\n     ';
+        expect(node.toString()).toMatch('<node>\n  A very fine day\n</node>');
+    });
+
+    it('escapes text content', function () {
+        node.text = '"<>&\'';
+        expect(node.toString()).toMatch('&quot;&lt;&gt;&amp;&apos;');
+    });
+
+    it('prints child nodes and text content', function () {
+        node.children.push(new XMLWriter.Node('child'));
+        node.text = 'Other content';
+        const output = node.toString();
+        expect(output).toMatch('<child/>');
+        expect(output).toMatch('Other content');
+    });
+});
